auth/api/iam/jar.go
/*
* Copyright (C) 2024 Nuts community
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
package iam
import (
"context"
"crypto"
"net/url"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/nuts-node/auth"
"github.com/nuts-foundation/nuts-node/auth/oauth"
cryptoNuts "github.com/nuts-foundation/nuts-node/crypto"
"github.com/nuts-foundation/nuts-node/vdr/resolver"
)
// requestObjectModifier is a function that modifies the Claims/params of an unsigned or signed (JWT) OAuth2 request
type requestObjectModifier func(claims map[string]string)
type jarRequest struct {
Claims oauthParameters `json:"claims"`
Client did.DID `json:"client_id"`
RequestURIMethod string `json:"request_uri_method"`
}
var _ JAR = &jar{}
type jar struct {
auth auth.AuthenticationServices
jwtSigner cryptoNuts.JWTSigner
keyResolver resolver.KeyResolver
}
type JAR interface {
// Create an unsigned request object.
// By default, it adds the following parameters:
// - client_id
// - iss
// - aud (if server is not nil)
// the request_uri_method is determined by the presence of a server (get) or not (post)
Create(client did.DID, server *did.DID, modifier requestObjectModifier) jarRequest
// Sign the jarRequest, which is available on jarRequest.Token.
// Returns an error if the jarRequest already contains a signed JWT.
// TODO: check if signature type of client is supported by the AS/wallet.
Sign(ctx context.Context, claims oauthParameters) (string, error)
// Parse and validate an incoming authorization request.
// Requests that do not conform to RFC9101 or OpenID4VP result in an error.
Parse(ctx context.Context, ownDID did.DID, q url.Values) (oauthParameters, error)
}
func (j jar) Create(client did.DID, server *did.DID, modifier requestObjectModifier) jarRequest {
return createJarRequest(client, server, modifier)
}
func createJarRequest(client did.DID, server *did.DID, modifier requestObjectModifier) jarRequest {
requestURIMethod := "post"
// default claims for JAR
params := map[string]string{
jwt.IssuerKey: client.String(),
oauth.ClientIDParam: client.String(),
}
if server != nil {
requestURIMethod = "get"
params[jwt.AudienceKey] = server.String()
}
// additional claims can be added by the caller
modifier(params)
oauthParams := make(oauthParameters, len(params))
for k, v := range params {
oauthParams[k] = v
}
return jarRequest{
Claims: oauthParams,
Client: client,
RequestURIMethod: requestURIMethod,
}
}
func (j jar) Sign(ctx context.Context, claims oauthParameters) (string, error) {
clientID := claims.get(oauth.ClientIDParam)
clientDID, err := did.ParseDID(clientID)
if err != nil {
return "", err
}
keyId, _, err := j.keyResolver.ResolveKey(*clientDID, nil, resolver.AssertionMethod)
if err != nil {
return "", err
}
return j.jwtSigner.SignJWT(ctx, claims, nil, keyId.String())
}
func (j jar) Parse(ctx context.Context, ownDID did.DID, q url.Values) (oauthParameters, error) {
var rawRequestObject string
var err error
if rawRequestObject = q.Get(oauth.RequestParam); rawRequestObject != "" {
if q.Get(oauth.RequestURIParam) != "" {
return nil, oauth.OAuth2Error{Code: oauth.InvalidRequest, Description: "claims 'request' and 'request_uri' are mutually exclusive"}
}
} else if requestURI := q.Get(oauth.RequestURIParam); requestURI != "" {
switch q.Get(oauth.RequestURIMethodParam) {
case "", "get": // empty string means client does not support request_uri_method, use 'get'
rawRequestObject, err = j.auth.IAMClient().RequestObjectByGet(ctx, requestURI)
if err != nil {
return nil, oauth.OAuth2Error{Code: oauth.InvalidRequestURI, Description: "failed to get Request Object", InternalError: err}
}
case "post":
issuerURL := ownDID.URI().URL
md, err := authorizationServerMetadata(ownDID, &issuerURL)
if err != nil {
// DB error
return nil, err
}
rawRequestObject, err = j.auth.IAMClient().RequestObjectByPost(ctx, requestURI, *md)
if err != nil {
return nil, oauth.OAuth2Error{Code: oauth.InvalidRequestURI, Description: "failed to get Request Object", InternalError: err}
}
default:
return nil, oauth.OAuth2Error{Code: oauth.InvalidRequestURIMethod, Description: "unsupported request_uri_method"}
}
} else {
// require_signed_request_object is true, so we reject anything that isn't
return nil, oauth.OAuth2Error{Code: oauth.InvalidRequest, Description: "authorization request are required to use signed request objects (RFC9101)"}
}
// already oauth.OAuth2Errors
return j.validate(ctx, rawRequestObject, q.Get(oauth.ClientIDParam))
}
// Validate validates a JAR (JWT Authorization Request) and returns the JWT claims.
// the client_id must match the signer of the JWT.
func (j jar) validate(ctx context.Context, rawToken string, clientId string) (oauthParameters, error) {
var signerKid string
// Parse and validate the JWT
token, err := cryptoNuts.ParseJWT(rawToken, func(kid string) (crypto.PublicKey, error) {
signerKid = kid
return j.keyResolver.ResolveKeyByID(kid, nil, resolver.AssertionMethod)
}, jwt.WithValidate(true))
if err != nil {
return nil, oauth.OAuth2Error{Code: oauth.InvalidRequestObject, Description: "request signature validation failed", InternalError: err}
}
claimsAsMap, err := token.AsMap(ctx)
if err != nil {
// very unlikely
return nil, oauth.OAuth2Error{Code: oauth.InvalidRequestObject, Description: "invalid request parameter", InternalError: err}
}
params := parseJWTClaims(claimsAsMap)
// check client_id claim, it must be the same as the client_id in the request
if clientId != params.get(oauth.ClientIDParam) {
return nil, oauth.OAuth2Error{Code: oauth.InvalidRequestObject, Description: "invalid client_id claim in signed authorization request"}
}
// check if the signer of the JWT is the client
signer, err := did.ParseDIDURL(signerKid)
if err != nil {
// very unlikely since the key has already been resolved
return nil, oauth.OAuth2Error{Code: oauth.InvalidRequestObject, Description: "invalid signer", InternalError: err}
}
if signer.DID.String() != clientId {
return nil, oauth.OAuth2Error{Code: oauth.InvalidRequestObject, Description: "client_id does not match signer of authorization request"}
}
return params, nil
}