mirror of
https://github.com/therootcompany/golib.git
synced 2026-03-29 12:35:10 +00:00
124 lines
3.1 KiB
Go
124 lines
3.1 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 dpop-jws demonstrates how to sign and decode a DPoP proof JWT
|
|
// (RFC 9449) with custom JOSE header fields.
|
|
//
|
|
// DPoP proof tokens use a custom typ ("dpop+jwt") and carry a server
|
|
// nonce in the header for replay protection. This example shows how to
|
|
// implement [jwt.SignableJWT] with a custom header struct.
|
|
//
|
|
// On the relying-party side, [jwt.DecodeRaw] + [jwt.RawJWT.UnmarshalHeader]
|
|
// gives you access to the custom fields.
|
|
//
|
|
// https://www.rfc-editor.org/rfc/rfc9449
|
|
package main
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"time"
|
|
|
|
"github.com/therootcompany/golib/auth/jwt"
|
|
)
|
|
|
|
// DPoPHeader extends the standard JOSE header with a DPoP nonce,
|
|
// as used in RFC 9449 (DPoP) proof tokens.
|
|
type DPoPHeader struct {
|
|
jwt.RFCHeader
|
|
Nonce string `json:"nonce,omitempty"`
|
|
}
|
|
|
|
// DPoPJWT is a custom JWT type that carries a DPoP header.
|
|
type DPoPJWT struct {
|
|
jwt.RawJWT
|
|
Header DPoPHeader
|
|
}
|
|
|
|
// GetHeader implements [jwt.VerifiableJWT].
|
|
func (d *DPoPJWT) GetHeader() jwt.RFCHeader { return d.Header.RFCHeader }
|
|
|
|
// SetHeader implements [jwt.SignableJWT]. It merges the signer's
|
|
// alg/kid into the DPoP header, then encodes the full protected header.
|
|
func (d *DPoPJWT) SetHeader(hdr jwt.Header) error {
|
|
d.Header.RFCHeader = *hdr.GetRFCHeader()
|
|
data, err := json.Marshal(d.Header)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
d.Protected = []byte(base64.RawURLEncoding.EncodeToString(data))
|
|
return nil
|
|
}
|
|
|
|
func main() {
|
|
pk, err := jwt.NewPrivateKey()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
signer, err := jwt.NewSigner([]*jwt.PrivateKey{pk})
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
claims := jwt.TokenClaims{
|
|
Iss: "https://auth.example.com",
|
|
Sub: "user123",
|
|
Aud: jwt.Listish{"https://api.example.com"},
|
|
Exp: time.Now().Add(time.Hour).Unix(),
|
|
IAt: time.Now().Unix(),
|
|
}
|
|
|
|
dpop := &DPoPJWT{Header: DPoPHeader{
|
|
RFCHeader: jwt.RFCHeader{Typ: "dpop+jwt"},
|
|
Nonce: "server-nonce-abc123",
|
|
}}
|
|
if err := dpop.SetClaims(&claims); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// SignJWT merges alg/kid from the key into our DPoP header.
|
|
if err := signer.SignJWT(dpop); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
tokenStr, err := jwt.Encode(dpop)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
fmt.Println("token:", tokenStr[:40]+"...")
|
|
|
|
// --- Relying party side: decode with custom header ---
|
|
|
|
raw, err := jwt.DecodeRaw(tokenStr)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
var h DPoPHeader
|
|
if err := raw.UnmarshalHeader(&h); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
fmt.Printf("alg: %s\n", h.Alg)
|
|
fmt.Printf("kid: %s\n", h.KID)
|
|
fmt.Printf("typ: %s\n", h.Typ)
|
|
fmt.Printf("nonce: %s\n", h.Nonce)
|
|
|
|
// Verify with the standard path.
|
|
verifier := signer.Verifier()
|
|
jws, err := jwt.Decode(tokenStr)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
if err := verifier.Verify(jws); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
fmt.Println("signature: valid")
|
|
}
|