mirror of
https://github.com/therootcompany/golib.git
synced 2026-03-29 13:13:57 +00:00
113 lines
3.3 KiB
Go
113 lines
3.3 KiB
Go
// Copyright 2026 AJ ONeal <aj@therootcompany.com> (https://therootcompany.com)
|
|
//
|
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
//
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
// Example oauth-access-token demonstrates OAuth 2.1 JWT access token
|
|
// validation per RFC 9068 using NewAccessTokenValidator with RequiredScopes.
|
|
//
|
|
// It mints an access token with JTI, ClientID, and Scope fields, then
|
|
// walks through the decode / verify / unmarshal / validate pipeline.
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"time"
|
|
|
|
"github.com/therootcompany/golib/auth/jwt"
|
|
)
|
|
|
|
func main() {
|
|
// --- Setup: create a signer + verifier for demonstration ---
|
|
pk, err := jwt.NewPrivateKey()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
signer, err := jwt.NewSigner([]*jwt.PrivateKey{pk})
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
verifier := signer.Verifier()
|
|
|
|
// --- Build access-token claims ---
|
|
// RFC 9068 requires iss, sub, aud, exp, iat, jti, and client_id.
|
|
claims := &jwt.TokenClaims{
|
|
Iss: "https://auth.example.com",
|
|
Sub: "user-42",
|
|
Aud: jwt.Listish{"https://api.example.com"},
|
|
Exp: time.Now().Add(time.Hour).Unix(),
|
|
IAt: time.Now().Unix(),
|
|
JTI: "tok-abc-123",
|
|
ClientID: "mobile-app",
|
|
Scope: jwt.SpaceDelimited{"read:messages", "write:messages", "profile"},
|
|
}
|
|
|
|
// --- Mint an access token (typ: at+jwt) ---
|
|
tok, err := jwt.NewAccessToken(claims)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
if err := signer.SignJWT(tok); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
tokenStr, err := tok.Encode()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
fmt.Println("access token:", tokenStr[:40]+"...")
|
|
|
|
// --- Decode (parse without verifying) ---
|
|
jws, err := jwt.Decode(tokenStr)
|
|
if err != nil {
|
|
log.Fatal("decode:", err)
|
|
}
|
|
fmt.Printf("header typ: %s\n", jws.GetHeader().Typ)
|
|
|
|
// --- Verify signature ---
|
|
if err := verifier.Verify(jws); err != nil {
|
|
log.Fatal("verify:", err)
|
|
}
|
|
fmt.Println("signature: OK")
|
|
|
|
// --- Unmarshal claims ---
|
|
var got jwt.TokenClaims
|
|
if err := jws.UnmarshalClaims(&got); err != nil {
|
|
log.Fatal("unmarshal:", err)
|
|
}
|
|
fmt.Printf("sub: %s client_id: %s jti: %s\n", got.Sub, got.ClientID, got.JTI)
|
|
fmt.Printf("scope: %v\n", got.Scope)
|
|
|
|
// --- Validate typ header + claims together ---
|
|
validator := jwt.NewAccessTokenValidator(
|
|
[]string{"https://auth.example.com"}, // allowed issuers
|
|
[]string{"https://api.example.com"}, // allowed audiences
|
|
0, // grace period (0 = default 2s)
|
|
)
|
|
validator.RequiredScopes = []string{"read:messages", "profile"}
|
|
|
|
// Thread header errors into Validate so all findings appear in one error.
|
|
hdr := jws.GetHeader()
|
|
var errs []error
|
|
errs = hdr.IsAllowedTyp(errs, []string{"at+jwt"})
|
|
if err := validator.Validate(errs, &got, time.Now()); err != nil {
|
|
log.Fatal("validate:", err)
|
|
}
|
|
fmt.Println("claims: OK (typ, iss, sub, aud, exp, iat, jti, client_id, scope all valid)")
|
|
|
|
// --- Demonstrate scope rejection ---
|
|
strict := jwt.NewAccessTokenValidator(
|
|
[]string{"https://auth.example.com"},
|
|
[]string{"https://api.example.com"},
|
|
0,
|
|
)
|
|
strict.RequiredScopes = []string{"admin:delete"} // not in the token
|
|
|
|
if err := strict.Validate(nil, &got, time.Now()); err != nil {
|
|
fmt.Println("expected rejection:", err)
|
|
}
|
|
}
|