diff --git a/auth/api/iam/api.go b/auth/api/iam/api.go index 8796295427..22b69c6ad9 100644 --- a/auth/api/iam/api.go +++ b/auth/api/iam/api.go @@ -323,18 +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. +// ownDID must be validated by the caller +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) + authzParams, 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 authzParams.get(oauth.ResponseTypeParam) { case responseTypeCode: // Options: // - Regular authorization code flow for EHR data access through access token, authentication of end-user using OpenID4VP. @@ -347,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 := authzParams.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, authzParams) } else { return nil, oauth.OAuth2Error{ Code: oauth.InvalidRequest, @@ -364,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, authzParams, walletOwnerType) default: // TODO: This should be a redirect? - redirectURI, _ := url.Parse(session.RedirectURI) + redirectURI, _ := url.Parse(authzParams.get(oauth.RedirectURIParam)) return nil, oauth.OAuth2Error{ Code: oauth.UnsupportedResponseType, RedirectURI: redirectURI, @@ -394,7 +396,7 @@ func (r Wrapper) GetRequestJWT(ctx context.Context, request GetRequestJWTRequest return nil, oauth.OAuth2Error{ Code: oauth.InvalidRequest, Description: "request object not found", - InternalError: errors.New("DID does not match client_id for requestID. Possible authorization RequestObject phishing"), + InternalError: errors.New("DID does not match client_id for requestID"), } } if ro.RequestURIMethod != "get" { @@ -406,6 +408,8 @@ func (r Wrapper) GetRequestJWT(ctx context.Context, request GetRequestJWTRequest 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 { return nil, oauth.OAuth2Error{ @@ -432,18 +436,18 @@ func (r Wrapper) PostRequestJWT(ctx context.Context, request PostRequestJWTReque Description: "request object not found", } } - if ro.RequestURIMethod != "post" { - return nil, oauth.OAuth2Error{ - Code: oauth.InvalidRequest, - Description: "used request_uri_method 'post' on a 'get' request_uri", - } - } // 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: "request object not found", - InternalError: errors.New("DID does not match client_id for requestID. Possible authorization RequestObject phishing"), + InternalError: errors.New("DID does not match client_id for requestID"), + } + } + if ro.RequestURIMethod != "post" { + return nil, oauth.OAuth2Error{ + Code: oauth.InvalidRequest, + Description: "used request_uri_method 'post' on a 'get' request_uri", } } @@ -689,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 { diff --git a/auth/api/iam/openid4vp.go b/auth/api/iam/openid4vp.go index b6aa924a42..24318d0818 100644 --- a/auth/api/iam/openid4vp.go +++ b/auth/api/iam/openid4vp.go @@ -337,7 +337,7 @@ func (r Wrapper) handleAuthorizeRequestFromVerifier(ctx context.Context, tenantD } // any error here is a server error, might need a fixup to prevent exposing to a user - return r.sendAndHandleDirectPost(ctx, tenantDID, *vp, *submission, responseURI, state) + return r.sendAndHandleDirectPost(ctx, userSession.Wallet.DID, *vp, *submission, responseURI, state) } func (r Wrapper) getClientMetadataFromRequest(ctx context.Context, params oauthParameters) (*oauth.OAuthClientMetadata, *oauth.OAuth2Error) { @@ -383,7 +383,7 @@ func (r Wrapper) getPresentationDefinitionFromRequest(ctx context.Context, param // sendAndHandleDirectPost sends OpenID4VP direct_post to the verifier. The verifier responds with a redirect to the client (including error fields if needed). // If the direct post fails, the user-agent will be redirected back to the client with an error. (Original redirect_uri). -func (r Wrapper) sendAndHandleDirectPost(ctx context.Context, walletDID did.DID, vp vc.VerifiablePresentation, presentationSubmission pe.PresentationSubmission, verifierResponseURI string, state string) (HandleAuthorizeRequestResponseObject, error) { +func (r Wrapper) sendAndHandleDirectPost(ctx context.Context, userWalletDID did.DID, vp vc.VerifiablePresentation, presentationSubmission pe.PresentationSubmission, verifierResponseURI string, state string) (HandleAuthorizeRequestResponseObject, error) { redirectURI, err := r.auth.IAMClient().PostAuthorizationResponse(ctx, vp, presentationSubmission, verifierResponseURI, state) if err != nil { return nil, err @@ -397,13 +397,8 @@ func (r Wrapper) sendAndHandleDirectPost(ctx context.Context, walletDID did.DID, } // Dispatch a new HTTP request to the local OpenID4VP wallet's authorization endpoint that includes request parameters, // but with openid4vp: as scheme. - originalRequest := ctx.Value(httpRequestContextKey{}).(*http.Request) - dispatchHttpRequest := *originalRequest - dispatchHttpRequest.URL = parsedRedirectURI - ctx = context.WithValue(ctx, httpRequestContextKey{}, &dispatchHttpRequest) - response, err := r.HandleAuthorizeRequest(ctx, HandleAuthorizeRequestRequestObject{ - Did: walletDID.String(), - }) + // The context contains data from the previous request. Usage by the handler will probably result in incorrect behavior. + response, err := r.handleAuthorizeRequest(ctx, userWalletDID, *parsedRedirectURI) if err != nil { return nil, err } diff --git a/auth/api/iam/user.go b/auth/api/iam/user.go index 5b0ccbf851..b90b94f28b 100644 --- a/auth/api/iam/user.go +++ b/auth/api/iam/user.go @@ -194,7 +194,7 @@ func (r Wrapper) loadUserSession(cookies CookieReader, tenantDID did.DID, preAut } // Note that the session itself does not have an expiration field: // it depends on the session store to clean up when it expires. - if !session.TenantDID.Equals(tenantDID) { + if !session.TenantDID.Equals(tenantDID) && !session.Wallet.DID.Equals(tenantDID) { return nil, fmt.Errorf("session belongs to another tenant (%s)", session.TenantDID) } // If the existing session was created for a pre-authorized user, the call to RequestUserAccessToken() must be diff --git a/auth/client/iam/client.go b/auth/client/iam/client.go index 6cb850e344..7ff81fa423 100644 --- a/auth/client/iam/client.go +++ b/auth/client/iam/client.go @@ -89,6 +89,7 @@ func (hb HTTPClient) PresentationDefinition(ctx context.Context, presentationDef return &presentationDefinition, hb.doRequest(ctx, request, &presentationDefinition) } +// RequestObject retrieves the Authorization Request Object from the requestURI using the GET method func (hb HTTPClient) RequestObject(ctx context.Context, requestURI string) (string, error) { request, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURI, nil) if err != nil { @@ -111,6 +112,8 @@ func (hb HTTPClient) RequestObject(ctx context.Context, requestURI string) (stri return string(data), err } +// RequestObjectPost retrieves the Authorization Request Object from the requestURI using the POST method. +// additional request parameters (wallet_metadata and wallet_nonce) are provided as url.Values. func (hb HTTPClient) RequestObjectPost(ctx context.Context, requestURI string, form url.Values) (string, error) { request, err := http.NewRequestWithContext(ctx, http.MethodPost, requestURI, strings.NewReader(form.Encode())) if err != nil {