dpop

package module
v1.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 4, 2023 License: MIT Imports: 14 Imported by: 0

README

go-dpop

Go Reference Coverage Status

OAuth 2.0 Demonstrating Proof of Possession (DPoP)

This package tries to implement RFC-9449

Supported key algorithms

Supported:

  • ES256, ES384, ES521
  • RS256, PS256
  • Ed25519

How to use

Authorization server

An authorization server needs to parse the incoming proof in order to associate the public key of the proof with the bound access token.
It should parse the proof to ensure that the sender of the proof has access to the private key.

import "github.com/AxisCommunications/go-dpop"

proof, err := dpop.Parse(proofString, dpop.POST, &httpUrl, dpop.ParseOptions{
    Nonce:      "",
    TimeWindow: &duration,
  })
// Check the error type to determine response
if err != nil {
  if ok := errors.Is(err, dpop.ErrInvalidProof); ok {
    // Return 'invalid_dpop_proof'
  }
}

// proof is valid, get public key to associate with access token
jkt := proof.PublicKey()

// Continue
Resource server

Resource servers need to do the same proof validation that authorization servers do but also check that the proof and access token are bound correctly.

import "github.com/AxisCommunications/go-dpop"

proof, err := dpop.Parse(proofString, dpop.POST, &httpUrl, dpop.ParseOptions{
    Nonce:      "",
    TimeWindow: &duration,
  })
// Check the error type to determine response
if err != nil {
  if ok := errors.Is(err, dpop.ErrInvalidProof); ok {
    // Return 'invalid_dpop_proof'
  }
}

// Hash the token with base64 and SHA256
// Get the access token JWT (introspect if needed)
// Parse the access token JWT and verify the signature

err = proof.Validate(accessTokenHash, accessTokenJWT)
// Check the error type to determine response
if err != nil {
  if ok := errors.Is(err, dpop.ErrInvalidProof); ok {
    // Return 'invalid_dpop_proof'
  }
  if ok := errors.Is(err, dpop.ErrIncorrectAccessTokenClaimsType); ok {
    // Return 'invalid_token'
  }
}

// Continue
Client

A client can generate proofs that authorization and resource servers can validate.

import "github.com/AxisCommunications/go-dpop"

// Setup the claims of the proof
claims := &dpop.ProofTokenClaims{
  RegisteredClaims: &jwt.RegisteredClaims{
    Issuer:    "test",
    Subject:   "sub",
    IssuedAt:  jwt.NewNumericDate(time.Now()),
    ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute)),
    ID:        "id",
  },
  Method: dpop.POST,
  URL:    "https://server.example.com/token",
}

// Create a key-pair to be used for signing
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
  ...
}

// Create a signed proof string
proofString, err := dpop.Create(jwt.SigningMethodES256, claims, privateKey)
if err != nil {
  ...
}

// Send the proof string in the 'DPoP' header to the server
Note on HMAC

Although this package can in theory support symmetric keys the DPoP draft does not allow private keys to be sent in the proof jwk header. As a symmetric key has no public key cryptography it can not be included in the proof, hence why it is unsupported.

Documentation

Index

Constants

View Source
const DEFAULT_TIME_WINDOW = time.Second * 30

Variables

View Source
var (
	// If proof validation failed for some reason a `ErrInvalidProof` error is returned.
	//
	// More specific reason for why validation failed will be added as a joined error on this error.
	ErrInvalidProof = errors.New("invalid_dpop_proof")

	// If the nonce was not provided a `ErrIncorrectNonce` error is returned.
	//
	// When this error is returned the the server needs to supply the client with a new nonce.
	ErrIncorrectNonce = errors.New("use_dpop_nonce")

	// The claims of the DPoP proof are invalid.
	ErrMissingClaims = errors.New("missing claims")

	// The `typ` header of the proof is invalid.
	ErrUnsupportedJWTType = errors.New("unsupported jwt type")

	// The `htm` and `htu` headers of the proof target the wrong resource.
	ErrIncorrectHTTPTarget = errors.New("incorrect http target")

	// The proof has expired.
	ErrExpired = errors.New("proof has expired")

	// The proof is issued too far into the future.
	ErrFuture = errors.New("proof is issued too far into the future")

	// The proof claims are not of correct type
	ErrIncorrectClaimsType = errors.New("incorrect claims type")

	// The proof missing the `ath` claim
	ErrMissingAth = errors.New("missing 'ath' claim")

	// The proof 'ath' claim does not match bound access token
	ErrAthMismatch = errors.New("ath mismatch")

	// The proof is missing the `jwk` public key header
	ErrMissingJWK = errors.New("missing 'jwk' header")

	// The proof 'jwk' public header does not match supplied jkt
	ErrIncorrectJKT = errors.New("incorrect 'jkt'")

	// The bound token 'jkt' claim does not match public key in proof
	ErrJWKMismatch = errors.New("key mismatch")

	// The bound access token claims are not of correct type
	ErrIncorrectAccessTokenClaimsType = errors.New("incorrect access token claims type")

	// The proof public key has an unsupported curve
	ErrUnsupportedCurve = errors.New("unsupported curve")

	// The proof uses an unsupported key algorithm
	ErrUnsupportedKeyAlgorithm = errors.New("unsupported key algorithm")
)

Functions

func Create

func Create(method jwt.SigningMethod, claims ProofClaims, privateKey crypto.Signer) (string, error)

Creates a DPoP proof for the given claims.

For custom claims it is recommended to embedd the 'ProofTokenClaims'.

Types

type BoundAccessTokenClaims

type BoundAccessTokenClaims struct {
	*jwt.RegisteredClaims

	// the `cnf` (Confirmation) claim. See https://datatracker.ietf.org/doc/html/draft-ietf-oauth-dpop#section-6.1
	Confirmation Confirmation `json:"cnf"`
}

These claims contains fields that are required to be present in bound access tokens.

If there is a need for custom claims this can be embedded in custom claims to ensure that claims are still possible to validate with the Validate function.

func (*BoundAccessTokenClaims) GetJWKThumbprint

func (c *BoundAccessTokenClaims) GetJWKThumbprint() (string, error)

Implement the BoundClaims interface.

func (*BoundAccessTokenClaims) Validate

func (c *BoundAccessTokenClaims) Validate() error

BoundAccessTokenClaims implements the 'ClaimsValidator' interface from golang-jwt/jwt.

This ensures that bound tokens has the required JWK thumbprint when parsed with 'ParseWithClaims'

type BoundClaims

type BoundClaims interface {
	jwt.Claims
	GetJWKThumbprint() (string, error)
}

This interface allows for custom claims to be used in bound tokens.

As long as any custom claims extends the 'BoundAccessTokenClaims' they will implement this interface and 'Validate' should handle them correctly

type Confirmation

type Confirmation struct {
	// the `jkt` (JWK Thumbprint) claim. See https://datatracker.ietf.org/doc/html/draft-ietf-oauth-dpop#section-6.1
	JWKThumbprint string `json:"jkt"`
}

type HTTPVerb

type HTTPVerb string

HTTPVerb is a convenience for determining the HTTP method of a request. This package defines the all available HTTP verbs which can be used when calling the Parse function.

const (
	GET     HTTPVerb = "GET"
	POST    HTTPVerb = "POST"
	PUT     HTTPVerb = "PUT"
	DELETE  HTTPVerb = "DELETE"
	PATCH   HTTPVerb = "PATCH"
	HEAD    HTTPVerb = "HEAD"
	OPTIONS HTTPVerb = "OPTIONS"
	TRACE   HTTPVerb = "TRACE"
	CONNECT HTTPVerb = "CONNECT"
)

HTTP method supported by the package.

type ParseOptions

type ParseOptions struct {
	// The expected nonce if the authorization server has issued a nonce.
	Nonce string

	// Used to control if the `iat` field is used to control the proof age.
	// If set to true the authorization server has to validate the nonce timestamp itself.
	NonceHasTimestamp bool

	// The allowed age of the proof. Defaults to 1 minute if not specified.
	TimeWindow *time.Duration

	// dpop_jkt parameter that is optionally sent by the client to the authorization server on token request.
	// If set the proof proof-of-possession public key needs to match or the proof is rejected.
	JKT string
}

ParseOptions and its contents are optional for the Parse function.

type Proof

type Proof struct {
	*jwt.Token
	HashedPublicKey string
}

Represents a DPoP proof, if acquired through the Parse function it should be a valid DPoP proof.

However if a bound access token was recieved with the proof the Validate function needs be used to verify that the proof is valid for the access token.

func Parse

func Parse(
	tokenString string,
	httpMethod HTTPVerb,
	httpURL *url.URL,
	opts ParseOptions,
) (*Proof, error)

Parse translates a DPoP proof string into a JWT token and parses it with the jwt package (github.com/golang-jwt/jwt/v5). It will also validate the proof according to https://datatracker.ietf.org/doc/html/rfc9449#section-4.3 but not check whether the proof matches a bound access token. It also assumes point 1 is checked by the calling application.

Protected resources should use the 'Validate' function on the returned proof to ensure that the proof matches any bound access token.

func (*Proof) PublicKey

func (t *Proof) PublicKey() string

Get the public key from the proof.

The public key string is base64 and url encoded according to https://datatracker.ietf.org/doc/html/draft-ietf-oauth-dpop#section-6.1 in order to help comparison of proof public key and 'jkt' claim of a bound access token.

An authorization server can use this to get the 'jkt' value it should encode in the bound access token.

func (*Proof) Validate

func (t *Proof) Validate(accessTokenHash []byte, boundAccessTokenJWT *jwt.Token) error

Validate takes a bound access token and validate that the token is bound correctly to this DPoP proof. This satisfies point 12 in https://datatracker.ietf.org/doc/html/draft-ietf-oauth-dpop#section-4.3

Validate needs to be called by a protected resource after parsing the DPoP proof. A bound access token needs to be introspected by the resource server in order for claims to be availabe for validation.

The bound access token should be validated before calling this function and the claims of the introspected bound access token needs to be of the BoundAccessTokenClaims type.

The access token hash needs to be a URL encoded SHA256 hash of the access token.

If no error is returned the proof is valid for the supplied bound token.

type ProofClaims

type ProofClaims interface {
	jwt.Claims
	GetAccessTokenHash() (string, error)
}

This interface allows for custom claims to be used in proof tokens.

As long as any custom claims extends the 'ProofTokenClaims' they will implement this interface and 'Validate' should handle them correctly

type ProofTokenClaims

type ProofTokenClaims struct {
	*jwt.RegisteredClaims

	// the `htm` (HTTP Method) claim. See https://datatracker.ietf.org/doc/html/draft-ietf-oauth-dpop#section-4.2
	Method HTTPVerb `json:"htm"`

	// the `htu` (HTTP URL) claim. See https://datatracker.ietf.org/doc/html/draft-ietf-oauth-dpop#section-4.2
	URL string `json:"htu"`

	// the `ath` (Authorization Token Hash) claim. See https://datatracker.ietf.org/doc/html/draft-ietf-oauth-dpop#section-4.2
	AccessTokenHash string `json:"ath,omitempty"`

	// the `nonce` claim. See https://datatracker.ietf.org/doc/html/draft-ietf-oauth-dpop#section-4.2
	Nonce string `json:"nonce,omitempty"`
}

These claims contains the standard fields of a DPoP proof claim.

If there is a need for custom claims this can be embedded in custom claims to ensure that claims are still possible to validate with the Validate function.

func (ProofTokenClaims) GetAccessTokenHash

func (p ProofTokenClaims) GetAccessTokenHash() (string, error)

Implement the ProofClaims interface.

Directories

Path Synopsis
examples

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL