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

added authorize endpoint as specified by rfc6549 authorization code #2626

Merged
merged 11 commits into from
Dec 12, 2023
6 changes: 2 additions & 4 deletions auth/api/auth/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,15 +311,13 @@ func (w Wrapper) RequestAccessToken(ctx context.Context, request RequestAccessTo
func (w Wrapper) CreateAccessToken(ctx context.Context, request CreateAccessTokenRequestObject) (CreateAccessTokenResponseObject, error) {

if request.Body.GrantType != client.JwtBearerGrantType {
errDesc := fmt.Sprintf("grant_type must be: '%s'", client.JwtBearerGrantType)
errorResponse := oauth.ErrorResponse{Error: errOauthUnsupportedGrant, Description: &errDesc}
errorResponse := oauth.OAuth2Error{Code: errOauthUnsupportedGrant, Description: fmt.Sprintf("grant_type must be: '%s'", client.JwtBearerGrantType)}
return CreateAccessToken400JSONResponse(errorResponse), nil
}

const jwtPattern = `^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$`
if matched, err := regexp.Match(jwtPattern, []byte(request.Body.Assertion)); !matched || err != nil {
errDesc := "Assertion must be a valid encoded jwt"
errorResponse := AccessTokenRequestFailedResponse{Error: errOauthInvalidGrant, Description: &errDesc}
errorResponse := AccessTokenRequestFailedResponse{Code: errOauthInvalidGrant, Description: "Assertion must be a valid encoded jwt"}
return CreateAccessToken400JSONResponse(errorResponse), nil
}

Expand Down
21 changes: 15 additions & 6 deletions auth/api/auth/v1/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ type TestContext struct {
contractClientMock *services.MockContractNotary
authzServerMock *oauth.MockAuthorizationServer
relyingPartyMock *oauth.MockRelyingParty
verifierMock *oauth.MockVerifier
wrapper Wrapper
cedentialResolverMock *vcr.MockResolver
audit context.Context
Expand All @@ -65,6 +66,7 @@ type mockAuthClient struct {
contractNotary *services.MockContractNotary
authzServer *oauth.MockAuthorizationServer
relyingParty *oauth.MockRelyingParty
verifier *oauth.MockVerifier
}

func (m *mockAuthClient) V2APIEnabled() bool {
Expand All @@ -79,6 +81,10 @@ func (m *mockAuthClient) RelyingParty() oauth.RelyingParty {
return m.relyingParty
}

func (m *mockAuthClient) Verifier() oauth.Verifier {
return m.verifier
}

func (m *mockAuthClient) ContractNotary() services.ContractNotary {
return m.contractNotary
}
Expand All @@ -97,13 +103,15 @@ func createContext(t *testing.T) *TestContext {
contractNotary := services.NewMockContractNotary(ctrl)
authzServer := oauth.NewMockAuthorizationServer(ctrl)
relyingParty := oauth.NewMockRelyingParty(ctrl)
verifier := oauth.NewMockVerifier(ctrl)
mockCredentialResolver := vcr.NewMockResolver(ctrl)

authMock := &mockAuthClient{
ctrl: ctrl,
contractNotary: contractNotary,
authzServer: authzServer,
relyingParty: relyingParty,
verifier: verifier,
}

requestCtx := audit.TestContext()
Expand All @@ -115,6 +123,7 @@ func createContext(t *testing.T) *TestContext {
contractClientMock: contractNotary,
authzServerMock: authzServer,
relyingPartyMock: relyingParty,
verifierMock: verifier,
cedentialResolverMock: mockCredentialResolver,
wrapper: Wrapper{Auth: authMock, CredentialResolver: mockCredentialResolver},
audit: requestCtx,
Expand Down Expand Up @@ -554,7 +563,7 @@ func TestWrapper_CreateAccessToken(t *testing.T) {
params := CreateAccessTokenRequest{GrantType: "unknown type"}

errorDescription := "grant_type must be: 'urn:ietf:params:oauth:grant-type:jwt-bearer'"
expectedResponse := CreateAccessToken400JSONResponse{Description: &errorDescription, Error: errOauthUnsupportedGrant}
expectedResponse := CreateAccessToken400JSONResponse{Description: errorDescription, Code: errOauthUnsupportedGrant}

response, err := ctx.wrapper.CreateAccessToken(ctx.audit, CreateAccessTokenRequestObject{Body: &params})

Expand All @@ -568,7 +577,7 @@ func TestWrapper_CreateAccessToken(t *testing.T) {
params := CreateAccessTokenRequest{GrantType: "urn:ietf:params:oauth:grant-type:jwt-bearer", Assertion: "invalid jwt"}

errorDescription := "Assertion must be a valid encoded jwt"
expectedResponse := CreateAccessToken400JSONResponse{Description: &errorDescription, Error: errOauthInvalidGrant}
expectedResponse := CreateAccessToken400JSONResponse{Description: errorDescription, Code: errOauthInvalidGrant}

response, err := ctx.wrapper.CreateAccessToken(ctx.audit, CreateAccessTokenRequestObject{Body: &params})

Expand All @@ -582,11 +591,11 @@ func TestWrapper_CreateAccessToken(t *testing.T) {
params := CreateAccessTokenRequest{GrantType: "urn:ietf:params:oauth:grant-type:jwt-bearer", Assertion: validJwt}

errorDescription := "oh boy"
expectedResponse := CreateAccessToken400JSONResponse{Description: &errorDescription, Error: errOauthInvalidRequest}
expectedResponse := CreateAccessToken400JSONResponse{Description: errorDescription, Code: errOauthInvalidRequest}

ctx.authzServerMock.EXPECT().CreateAccessToken(ctx.audit, services.CreateAccessTokenRequest{RawJwtBearerToken: validJwt}).Return(nil, &oauth2.ErrorResponse{
Description: &errorDescription,
Error: errOauthInvalidRequest,
ctx.authzServerMock.EXPECT().CreateAccessToken(ctx.audit, services.CreateAccessTokenRequest{RawJwtBearerToken: validJwt}).Return(nil, &oauth2.OAuth2Error{
Description: errorDescription,
Code: errOauthInvalidRequest,
})

response, err := ctx.wrapper.CreateAccessToken(ctx.audit, CreateAccessTokenRequestObject{Body: &params})
Expand Down
2 changes: 1 addition & 1 deletion auth/api/auth/v1/client/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,4 @@ type VerifiablePresentation = vc.VerifiablePresentation
type AccessTokenResponse = oauth.TokenResponse

// AccessTokenRequestFailedResponse is an alias to use from within the API
type AccessTokenRequestFailedResponse = oauth.ErrorResponse
type AccessTokenRequestFailedResponse = oauth.OAuth2Error
2 changes: 1 addition & 1 deletion auth/api/auth/v1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ type VerifiablePresentation = vc.VerifiablePresentation
// AccessTokenResponse is an alias to use from within the API
type AccessTokenResponse = oauth.TokenResponse

type AccessTokenRequestFailedResponse = oauth.ErrorResponse
type AccessTokenRequestFailedResponse = oauth.OAuth2Error
45 changes: 26 additions & 19 deletions auth/api/iam/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func New(authInstance auth.AuthenticationServices, vcrInstance vcr.VCR, vdrInsta
}
}

func (r *Wrapper) Routes(router core.EchoRouter) {
func (r Wrapper) Routes(router core.EchoRouter) {
RegisterHandlers(router, NewStrictHandler(r, []StrictMiddlewareFunc{
func(f StrictHandlerFunc, operationID string) StrictHandlerFunc {
return func(ctx echo.Context, request interface{}) (response interface{}, err error) {
Expand Down Expand Up @@ -166,7 +166,7 @@ func (r Wrapper) HandleTokenRequest(ctx context.Context, request HandleTokenRequ
}

// IntrospectAccessToken allows the resource server (XIS/EHR) to introspect details of an access token issued by this node
func (r Wrapper) IntrospectAccessToken(ctx context.Context, request IntrospectAccessTokenRequestObject) (IntrospectAccessTokenResponseObject, error) {
func (r Wrapper) IntrospectAccessToken(_ context.Context, request IntrospectAccessTokenRequestObject) (IntrospectAccessTokenResponseObject, error) {
// Validate token
if request.Body.Token == "" {
// Return 200 + 'Active = false' when token is invalid or malformed
Expand Down Expand Up @@ -249,7 +249,6 @@ func (r Wrapper) HandleAuthorizeRequest(ctx context.Context, request HandleAutho
if err != nil {
return nil, err
}
// Create session object to be passed to handler

// Workaround: deepmap codegen doesn't support dynamic query parameters.
// See https://github.com/deepmap/oapi-codegen/issues/1129
Expand All @@ -268,15 +267,31 @@ func (r Wrapper) HandleAuthorizeRequest(ctx context.Context, request HandleAutho
Description: "redirect_uri is required",
}
}
// todo: store session in database?

switch session.ResponseType {
case responseTypeCode:
// Options:
// - Regular authorization code flow for EHR data access through access token, authentication of end-user using OpenID4VP.
// - OpenID4VCI; authorization code flow for credential issuance to (end-user) wallet
// - OpenID4VP, vp_token is sent in Token Response; authorization code flow for presentation exchange (not required a.t.m.)
// TODO: Switch on parameters to right flow
panic("not implemented")

// TODO: officially flow switching has to be determined by the client_id
// registered client_ids should list which flow they support
// client registration could be done via rfc7591....
// for now we switch on client_id format.
// 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
reinkrul marked this conversation as resolved.
Show resolved Hide resolved
if strings.HasPrefix(clientId, "did:web:") {
// client is a cloud wallet with user
return r.handleAuthorizeRequestFromHolder(ctx, *ownDID, params)
} else {
return nil, oauth.OAuth2Error{
Code: oauth.InvalidRequest,
Description: "client_id must be a did:web",
}
}
case responseTypeVPToken:
// Options:
// - OpenID4VP flow, vp_token is sent in Authorization Response
Expand All @@ -289,28 +304,20 @@ func (r Wrapper) HandleAuthorizeRequest(ctx context.Context, request HandleAutho
return r.handlePresentationRequest(params, session)
default:
// TODO: This should be a redirect?
redirectURI, _ := url.Parse(session.RedirectURI)
return nil, oauth.OAuth2Error{
Code: oauth.UnsupportedResponseType,
RedirectURI: session.RedirectURI,
RedirectURI: redirectURI,
}
}
}

// OAuthAuthorizationServerMetadata returns the Authorization Server's metadata
func (r Wrapper) OAuthAuthorizationServerMetadata(ctx context.Context, request OAuthAuthorizationServerMetadataRequestObject) (OAuthAuthorizationServerMetadataResponseObject, error) {
ownDID := r.idToDID(request.Id)
owned, err := r.vdr.IsOwner(ctx, ownDID)
_, err := r.idToOwnedDID(ctx, request.Id)
if err != nil {
if resolver.IsFunctionalResolveError(err) {
return nil, core.NotFoundError("authz server metadata: %w", err)
}
log.Logger().WithField("did", ownDID.String()).Errorf("authz server metadata: failed to assert ownership of did: %s", err.Error())
return nil, core.Error(500, "authz server metadata: %w", err)
}
if !owned {
return nil, core.NotFoundError("authz server metadata: did not owned")
return nil, err
}

identity := r.auth.PublicURL().JoinPath("iam", request.Id)

return OAuthAuthorizationServerMetadata200JSONResponse(authorizationServerMetadata(*identity)), nil
Expand Down Expand Up @@ -432,6 +439,6 @@ func (r Wrapper) identityURL(id string) *url.URL {
return r.auth.PublicURL().JoinPath("iam", id)
}

func (r *Wrapper) accessTokenStore() storage.SessionStore {
func (r Wrapper) accessTokenStore() storage.SessionStore {
return r.storageEngine.GetSessionDatabase().GetStore(accessTokenValidity, "accesstoken")
}
Loading
Loading