Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support request_uri_method=post #3102

Merged
merged 8 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 102 additions & 54 deletions auth/api/iam/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,15 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/google/uuid"
"html/template"
"net/http"
"net/url"
"strings"
"time"

"github.com/google/uuid"
"github.com/labstack/echo/v4"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/go-did/vc"
"github.com/nuts-foundation/nuts-node/audit"
Expand Down Expand Up @@ -322,17 +323,20 @@ func (r Wrapper) HandleAuthorizeRequest(ctx context.Context, request HandleAutho
// Workaround: deepmap codegen doesn't support dynamic query parameters.
// See https://github.com/deepmap/oapi-codegen/issues/1129
httpRequest := ctx.Value(httpRequestContextKey{}).(*http.Request)
queryParams := httpRequest.URL.Query()
return r.handleAuthorizeRequest(ctx, *ownDID, *httpRequest.URL)
}

// handleAuthorizeRequest handles calls to the authorization endpoint for starting an authorization code flow.
// The caller must ensure ownDID is actually owned by this node.
func (r Wrapper) handleAuthorizeRequest(ctx context.Context, ownDID did.DID, request url.URL) (HandleAuthorizeRequestResponseObject, error) {
// parse and validate as JAR (RFC9101, JWT Authorization Request)
authzParams, err := r.jar.Parse(ctx, *ownDID, queryParams)
requestObject, err := r.jar.Parse(ctx, ownDID, request.Query())
if err != nil {
// already an oauth.OAuth2Error
return nil, err
}

session := createSession(authzParams, *ownDID)

switch session.ResponseType {
switch requestObject.get(oauth.ResponseTypeParam) {
case responseTypeCode:
// Options:
// - Regular authorization code flow for EHR data access through access token, authentication of end-user using OpenID4VP.
Expand All @@ -345,10 +349,10 @@ func (r Wrapper) HandleAuthorizeRequest(ctx context.Context, request HandleAutho
// when client_id is a did:web, it is a cloud/server wallet
// otherwise it's a normal registered client which we do not support yet
// Note: this is the user facing OpenID4VP flow with a "vp_token" responseType, the demo uses the "vp_token id_token" responseType
clientId := session.ClientID
clientId := requestObject.get(oauth.ClientIDParam)
if strings.HasPrefix(clientId, "did:web:") {
// client is a cloud wallet with user
return r.handleAuthorizeRequestFromHolder(ctx, *ownDID, authzParams)
return r.handleAuthorizeRequestFromHolder(ctx, ownDID, requestObject)
} else {
return nil, oauth.OAuth2Error{
Code: oauth.InvalidRequest,
Expand All @@ -362,13 +366,13 @@ func (r Wrapper) HandleAuthorizeRequest(ctx context.Context, request HandleAutho
// Requests to user wallets can then be rendered as QR-code (or use a cloud wallet).
// Note that it can't be called from the outside, but only by internal dispatch (since Echo doesn't handle openid4vp:, obviously).
walletOwnerType := pe.WalletOwnerOrganization
if strings.HasPrefix(httpRequest.URL.String(), "openid4vp:") {
if strings.HasPrefix(request.String(), "openid4vp:") {
walletOwnerType = pe.WalletOwnerUser
}
return r.handleAuthorizeRequestFromVerifier(ctx, *ownDID, authzParams, walletOwnerType)
return r.handleAuthorizeRequestFromVerifier(ctx, ownDID, requestObject, walletOwnerType)
default:
// TODO: This should be a redirect?
redirectURI, _ := url.Parse(session.RedirectURI)
redirectURI, _ := url.Parse(requestObject.get(oauth.RedirectURIParam))
return nil, oauth.OAuth2Error{
Code: oauth.UnsupportedResponseType,
RedirectURI: redirectURI,
Expand All @@ -382,25 +386,37 @@ func (r Wrapper) GetRequestJWT(ctx context.Context, request GetRequestJWTRequest
ro := new(jarRequest)
err := r.authzRequestObjectStore().Get(request.Id, ro)
gerardsn marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, err
return nil, oauth.OAuth2Error{
Code: oauth.InvalidRequest,
Description: "request object not found",
}
}
// compare raw strings, don't waste a db call to see if we own the request.Did.
if ro.Client.String() != request.Did {
return nil, errors.New("invalid request")
return nil, oauth.OAuth2Error{
Code: oauth.InvalidRequest,
Description: "client_id does not match request",
InternalError: errors.New("DID does not match client_id for requestID"),
gerardsn marked this conversation as resolved.
Show resolved Hide resolved
}
}
if ro.RequestURIMethod != "get" {
// TODO: wallet does not support `request_uri_method=post`. Signing the current jarRequest would leave it without 'aud'.
// is this acceptable or should it fail?
// is this acceptable, should it fail, or does it default to using staticAuthorizationServerMetadata.
return nil, oauth.OAuth2Error{
Code: oauth.InvalidRequest,
Description: "used request_uri_method 'get' on a 'post' request_uri",
InternalError: errors.New("wrong 'request_uri_method' authorization server or wallet probably does not support 'request_uri_method'"),
}
}

// TODO: supported signature types should be checked
token, err := r.jar.Sign(ctx, ro.Claims)
if err != nil {
// TODO: oauth.OAuth2Error?
return nil, err
return nil, oauth.OAuth2Error{
Code: oauth.ServerError,
Description: "failed to sign authorization RequestObject",
gerardsn marked this conversation as resolved.
Show resolved Hide resolved
InternalError: err,
}
}
return GetRequestJWT200ApplicationoauthAuthzReqJwtResponse{
Body: bytes.NewReader([]byte(token)),
Expand All @@ -412,7 +428,53 @@ func (r Wrapper) GetRequestJWT(ctx context.Context, request GetRequestJWTRequest
// Extension of OpenID 4 Verifiable Presentations (OpenID4VP) on
// RFC9101: The OAuth 2.0 Authorization Framework: JWT-Secured Authorization Request (JAR).
func (r Wrapper) PostRequestJWT(ctx context.Context, request PostRequestJWTRequestObject) (PostRequestJWTResponseObject, error) {
gerardsn marked this conversation as resolved.
Show resolved Hide resolved
return nil, errors.New("not implemented")
ro := new(jarRequest)
err := r.authzRequestObjectStore().Get(request.Id, ro)
gerardsn marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, oauth.OAuth2Error{
Code: oauth.InvalidRequest,
Description: "request object not found",
}
}
// compare raw strings, don't waste a db call to see if we own the request.Did.
if ro.Client.String() != request.Did {
return nil, oauth.OAuth2Error{
Code: oauth.InvalidRequest,
Description: "client_id does not match request",
InternalError: errors.New("DID does not match client_id for requestID"),
gerardsn marked this conversation as resolved.
Show resolved Hide resolved
}
}
if ro.RequestURIMethod != "post" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

http.MethodPost

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

request_uri_method and http methods are both specced as case sensitive, but annoyingly use opposite casing. added a comment

return nil, oauth.OAuth2Error{
Code: oauth.InvalidRequest,
Description: "used request_uri_method 'post' on a 'get' request_uri",
}
}

walletMetadata := staticAuthorizationServerMetadata()
if request.Body != nil {
if request.Body.WalletMetadata != nil {
walletMetadata = *request.Body.WalletMetadata
}
if request.Body.WalletNonce != nil {
ro.Claims[oauth.WalletNonceParam] = *request.Body.WalletNonce
}
}
ro.Claims[jwt.AudienceKey] = walletMetadata.Issuer

// TODO: supported signature types should be checked
token, err := r.jar.Sign(ctx, ro.Claims)
if err != nil {
return nil, oauth.OAuth2Error{
Code: oauth.ServerError,
Description: "failed to sign authorization RequestObject",
gerardsn marked this conversation as resolved.
Show resolved Hide resolved
InternalError: err,
}
}
return PostRequestJWT200ApplicationoauthAuthzReqJwtResponse{
Body: bytes.NewReader([]byte(token)),
ContentLength: int64(len(token)),
}, nil
}

// OAuthAuthorizationServerMetadata returns the Authorization Server's metadata
Expand All @@ -438,17 +500,7 @@ func (r Wrapper) oauthAuthorizationServerMetadata(ctx context.Context, didAsStri
if err != nil {
return nil, err
}
identity, err := didweb.DIDToURL(*ownDID)
if err != nil {
return nil, err
}
oauth2BaseURL, err := createOAuth2BaseURL(*ownDID)
if err != nil {
// can't fail, already did DIDToURL above
return nil, err
}
md := authorizationServerMetadata(*identity, *oauth2BaseURL)
return &md, nil
return authorizationServerMetadata(*ownDID)
}

func (r Wrapper) GetTenantWebDID(_ context.Context, request GetTenantWebDIDRequestObject) (GetTenantWebDIDResponseObject, error) {
Expand Down Expand Up @@ -641,22 +693,6 @@ func (r Wrapper) RequestUserAccessToken(ctx context.Context, request RequestUser
}, nil
}

func createSession(params oauthParameters, ownDID did.DID) *OAuthSession {
session := OAuthSession{}
session.ClientID = params.get(oauth.ClientIDParam)
session.Scope = params.get(oauth.ScopeParam)
session.ClientState = params.get(oauth.StateParam)
session.RedirectURI = params.get(oauth.RedirectURIParam)
session.OwnDID = &ownDID
session.ResponseType = params.get(oauth.ResponseTypeParam)
session.PKCEParams = PKCEParams{
Challenge: params.get(oauth.CodeChallengeParam),
ChallengeMethod: params.get(oauth.CodeChallengeMethodParam),
}

return &session
}

func (r Wrapper) StatusList(ctx context.Context, request StatusListRequestObject) (StatusListResponseObject, error) {
requestDID, err := did.ParseDID(request.Did)
if err != nil {
Expand Down Expand Up @@ -843,15 +879,27 @@ func (r Wrapper) openidIssuerEndpoints(ctx context.Context, issuerDid did.DID) (
// - jwt.Audience
// - nonce
// any of these params can be overridden by the requestObjectModifier.
func (r Wrapper) CreateAuthorizationRequest(ctx context.Context, client did.DID, server did.DID, modifier requestObjectModifier) (*url.URL, error) {
// we want to make a call according to §4.1.1 of RFC6749, https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.1
// The URL should be listed in the verifier metadata under the "authorization_endpoint" key
metadata, err := r.auth.IAMClient().AuthorizationServerMetadata(ctx, server)
if err != nil {
return nil, fmt.Errorf("failed to retrieve remote OAuth Authorization Server metadata: %w", err)
func (r Wrapper) CreateAuthorizationRequest(ctx context.Context, client did.DID, server *did.DID, modifier requestObjectModifier) (*url.URL, error) {
// if the server is unknown/nil we are talking to a wallet.
// by default requireSignedRequestObject=true to make sure the produced Authorization Request URL does not exceed request URL limit on mobile devices
gerardsn marked this conversation as resolved.
Show resolved Hide resolved
metadata := new(oauth.AuthorizationServerMetadata)
if server != nil {
// we want to make a call according to §4.1.1 of RFC6749, https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.1
// The URL should be listed in the verifier metadata under the "authorization_endpoint" key
var err error
metadata, err = r.auth.IAMClient().AuthorizationServerMetadata(ctx, *server)
if err != nil {
return nil, fmt.Errorf("failed to retrieve remote OAuth Authorization Server metadata: %w", err)
}
} else {
// use static configuration until while we try to determine the wallet that will answer the authorization request. (user wallet / QR code flow)
gerardsn marked this conversation as resolved.
Show resolved Hide resolved
*metadata = staticAuthorizationServerMetadata()
// TODO: metadata.RequireSignedRequestObject == false.
// This means we send both a request_uri and add all params to the authorization request as query params.
// The resulting url is too long and will be rejected by mobile devices.
}
if len(metadata.AuthorizationEndpoint) == 0 {
return nil, fmt.Errorf("no authorization endpoint found in metadata for %s", server)
return nil, fmt.Errorf("no authorization endpoint found in metadata for %s", *server)
gerardsn marked this conversation as resolved.
Show resolved Hide resolved
}
endpoint, err := url.Parse(metadata.AuthorizationEndpoint)
if err != nil {
Expand All @@ -860,8 +908,8 @@ func (r Wrapper) CreateAuthorizationRequest(ctx context.Context, client did.DID,

// request_uri
requestURIID := nutsCrypto.GenerateNonce()
requestObj := r.jar.Create(client, &server, modifier)
if err = r.authzRequestObjectStore().Put(requestURIID, requestObj); err != nil {
requestObj := r.jar.Create(client, server, modifier)
if err := r.authzRequestObjectStore().Put(requestURIID, requestObj); err != nil {
return nil, err
}
baseURL, err := createOAuth2BaseURL(client)
Expand All @@ -883,7 +931,7 @@ func (r Wrapper) CreateAuthorizationRequest(ctx context.Context, client did.DID,
// else; unclear if AS has support for RFC9101, so also add all modifiers to the query itself
// left here for completeness, node 2 node interaction always uses JAR since the AS metadata has it hardcoded
// TODO: in the user flow we have no AS metadata, meaning that we add all params to the query.
// This is most likely going to fail on mobile devices due to request url length.
// This is most likely going to fail on mobile devices due to request url length.
modifier(params)
redirectURL := nutsHttp.AddQueryParams(*endpoint, params)
return &redirectURL, nil
Expand Down
Loading
Loading