diff --git a/api/api.yaml b/api/api.yaml index bb29617..7bb5ceb 100644 --- a/api/api.yaml +++ b/api/api.yaml @@ -97,6 +97,9 @@ paths: parameters: - $ref: '#/components/parameters/QueryState' - $ref: '#/components/parameters/ClientId' + - $ref: '#/components/parameters/Scope' + - $ref: '#/components/parameters/RequestMode' + - $ref: '#/components/parameters/RedirectPath' operationId: StartSIOPSameDevice summary: Starts the siop flow for credentials hold by the same device description: When the credential is already present in the requesting browser, the same-device flow can be used. It creates the login information and then redirects to the /authenticationresponse path. @@ -153,7 +156,6 @@ paths: responses: '204': description: Ok when it worked - /token: post: tags: @@ -302,6 +304,14 @@ components: schema: type: string example: https://my-app.com/request.jwt + RedirectPath: + name: redirect_path + description: If no redirect path is provided, an 'oid4vp' deeplink will be returned + in: query + required: false + schema: + type: string + example: / VpToken: name: vp_token description: base64URLEncoded VerifiablePresentation @@ -600,7 +610,7 @@ components: properties: grant_type: type: string - enum: ["authorization_code"] + enum: ["authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange"] code: type: string example: myRandomString @@ -609,15 +619,43 @@ components: format: uri description: Same uri as provided as callback in the original request. example: https://my-portal.com/auth_callback + resource: + type: string + format: uri + description: A URI that indicates the target service or resource where the client intends to use the requested security token. Resource + is ignored if the target client is provided as path parameter + audience: + type: string + description: The logical name of the target service where the client intends to use the requested security token. + scope: + type: array + items: + type: string + description: A list of space-delimited, case-sensitive strings, that allow the client to specify the desired scope of the requested security token in the context of the service or resource where the token will be used. + requested_token_type: + type: string + description: An identifier, for the type of the requested security token. + enum: ["urn:ietf:params:oauth:token-type:access_token"] + subject_token: + type: string + description: A security token that represents the identity of the party on behalf of whom the request is being made. + subject_token_type: + type: string + description: An identifier that indicates the type of the security token in the subject_token parameter. + enum: ["urn:eu:oidf:vp_token"] + required: + - grant_type TokenResponse: type: object properties: token_type: type: string enum: ["Bearer"] + issued_token_type: + type: string + enum: ["urn:ietf:params:oauth:token-type:access_token"] expires_in: type: number example: 3600 access_token: type: string - diff --git a/common/metadata.go b/common/metadata.go index 38b92f0..6de7a42 100644 --- a/common/metadata.go +++ b/common/metadata.go @@ -2,6 +2,9 @@ package common const TYPE_CODE = "authorization_code" const TYPE_VP_TOKEN = "vp_token" +const TYPE_TOKEN_EXCHANGE = "urn:ietf:params:oauth:grant-type:token-exchange" +const TYPE_VP_TOKEN_SUBJECT = "urn:eu:oidf:vp_token" +const TYPE_ACCESS_TOKEN = "urn:ietf:params:oauth:token-type:access_token" type OpenIDProviderMetadata struct { Issuer string `json:"issuer"` diff --git a/config/configClient.go b/config/configClient.go index 0c5267e..636fdc8 100644 --- a/config/configClient.go +++ b/config/configClient.go @@ -40,9 +40,10 @@ type ServicesResponse struct { type ConfiguredService struct { // Default OIDC scope to be used if none is specified - DefaultOidcScope string `json:"defaultOidcScope" mapstructure:"defaultOidcScope"` - ServiceScopes map[string]ScopeEntry `json:"oidcScopes" mapstructure:"oidcScopes"` - Id string `json:"id" mapstructure:"id"` + DefaultOidcScope string `json:"defaultOidcScope" mapstructure:"defaultOidcScope"` + ServiceScopes map[string]ScopeEntry `json:"oidcScopes" mapstructure:"oidcScopes"` + Id string `json:"id" mapstructure:"id"` + AuthorizationPath string `json:"authorizationPath,omitempty" mapstructure:"authorizationPath,omitempty"` } type ScopeEntry struct { diff --git a/config/configClient_test.go b/config/configClient_test.go index 4ae36bc..8dc2529 100644 --- a/config/configClient_test.go +++ b/config/configClient_test.go @@ -2,7 +2,7 @@ package config import ( "io" - "io/ioutil" + "os" "strings" "testing" @@ -20,7 +20,7 @@ func (mhc MockHttpClient) Get(url string) (resp *http.Response, err error) { } func readFile(filename string, t *testing.T) string { - data, err := ioutil.ReadFile("data/" + filename) + data, err := os.ReadFile("data/" + filename) if err != nil { t.Error("could not read file", err) } diff --git a/openapi/api_api.go b/openapi/api_api.go index fd5b410..ec5696a 100644 --- a/openapi/api_api.go +++ b/openapi/api_api.go @@ -41,6 +41,7 @@ var ErrorMessagNoGrantType = ErrorMessage{"no_grant_type_provided", "Token reque var ErrorMessageUnsupportedGrantType = ErrorMessage{"unsupported_grant_type", "Provided grant_type is not supported by the implementation."} var ErrorMessageNoCode = ErrorMessage{"no_code_provided", "Token requests require a code."} var ErrorMessageNoRedircetUri = ErrorMessage{"no_redirect_uri_provided", "Token requests require a redirect_uri."} +var ErrorMessageNoResource = ErrorMessage{"no_resource_provided", "When using token-exchange, resource is required to provide the client_id."} var ErrorMessageNoState = ErrorMessage{"no_state_provided", "Authentication requires a state provided as query parameter."} var ErrorMessageNoScope = ErrorMessage{"no_scope_provided", "Authentication requires a scope provided as a parameter."} var ErrorMessageNoNonce = ErrorMessage{"no_nonce_provided", "Authentication requires a nonce provided as a query parameter."} @@ -58,6 +59,8 @@ var ErrorMessageInvalidAudience = ErrorMessage{"invalid_audience", "Audience of var ErrorMessageUnsupportedAssertionType = ErrorMessage{"unsupported_assertion_type", "Assertion type is not supported."} var ErrorMessageInvalidClientAssertion = ErrorMessage{"invalid_client_assertion", "Provided client assertion is invalid."} var ErrorMessageInvalidTokenRequest = ErrorMessage{"invalid_token_request", "Token request has no redirect_uri and no valid client assertion."} +var ErrorMessageInvalidSubjectTokenType = ErrorMessage{"invalid_subject_token_type", "Token exchange is only supported for token type urn:eu:oidf:vp_token."} +var ErrorMessageInvalidRequestedTokenType = ErrorMessage{"invalid_requested_token_type", "Token exchange is only supported for requesting tokens of type urn:ietf:params:oauth:token-type:access_token."} func getApiVerifier() verifier.Verifier { if apiVerifier == nil { @@ -98,12 +101,20 @@ func GetToken(c *gin.Context) { return } - if grantType == common.TYPE_CODE { + switch grantType { + case common.TYPE_CODE: handleTokenTypeCode(c) - } else if grantType == common.TYPE_VP_TOKEN { + case common.TYPE_VP_TOKEN: handleTokenTypeVPToken(c, c.GetHeader("client_id")) - } else { - c.AbortWithStatusJSON(400, ErrorMessageUnsupportedGrantType) + case common.TYPE_TOKEN_EXCHANGE: + resource, resourceExists := c.GetPostForm("resource") + if !resourceExists { + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageNoResource) + return + } + handleTokenTypeTokenExchange(c, resource) + default: + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageUnsupportedGrantType) } } @@ -114,62 +125,90 @@ func GetTokenForService(c *gin.Context) { grantType, grantTypeExists := c.GetPostForm("grant_type") if !grantTypeExists { logging.Log().Debug("No grant_type present in the request.") - c.AbortWithStatusJSON(400, ErrorMessagNoGrantType) + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessagNoGrantType) return } - if grantType == common.TYPE_CODE { + switch grantType { + case common.TYPE_CODE: handleTokenTypeCode(c) - } else if grantType == common.TYPE_VP_TOKEN { + case common.TYPE_VP_TOKEN: handleTokenTypeVPToken(c, c.Param("service_id")) - } else { - c.AbortWithStatusJSON(400, ErrorMessageUnsupportedGrantType) + case common.TYPE_TOKEN_EXCHANGE: + handleTokenTypeTokenExchange(c, c.Param("service_id")) + default: + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageUnsupportedGrantType) + } +} + +func handleTokenTypeTokenExchange(c *gin.Context, clientId string) { + subjectTokenType, subjectTokenTypeExists := c.GetPostForm("subject_token_type") + if !subjectTokenTypeExists || subjectTokenType != common.TYPE_VP_TOKEN_SUBJECT { + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageInvalidSubjectTokenType) + return + } + requestedTokenType, requestedTokenTypeExists := c.GetPostForm("requested_token_type") + if requestedTokenTypeExists && requestedTokenType != common.TYPE_ACCESS_TOKEN { + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageInvalidRequestedTokenType) + return } + + scopes := getScopesFromRequest(c) + if len(scopes) == 0 { + return + } + + audience, audienceExists := c.GetPostForm("audience") + if !audienceExists { + audience = clientId + } + + subjectToken, subjectTokenExists := c.GetPostForm("subject_token") + if !subjectTokenExists { + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageNoToken) + return + } + + logging.Log().Debugf("Got token %s", subjectToken) + + verifiyVPToken(c, subjectToken, clientId, scopes, audience) } func handleTokenTypeVPToken(c *gin.Context, clientId string) { - var requestBody TokenRequestBody vpToken, vpTokenExists := c.GetPostForm("vp_token") if !vpTokenExists { logging.Log().Debug("No vp token present in the request.") - c.AbortWithStatusJSON(400, ErrorMessageNoToken) + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageNoToken) return } logging.Log().Warnf("Got token %s", vpToken) - // not used at the moment - // presentationSubmission, presentationSubmissionExists := c.GetPostForm("presentation_submission") - // if !presentationSubmissionExists { - // logging.Log().Debug("No presentation submission present in the request.") - // c.AbortWithStatusJSON(400, ErrorMessageNoPresentationSubmission) - // return - //} - - scope, scopeExists := c.GetPostForm("scope") - if !scopeExists { - logging.Log().Debug("No scope present in the request.") - c.AbortWithStatusJSON(400, ErrorMessageNoScope) + scopes := getScopesFromRequest(c) + if len(scopes) == 0 { return } + verifiyVPToken(c, vpToken, clientId, scopes, clientId) +} + +func verifiyVPToken(c *gin.Context, vpToken string, clientId string, scopes []string, audience string) { + presentation, err := extractVpFromToken(c, vpToken) if err != nil { logging.Log().Warnf("Was not able to extract the credentials from the vp_token. E: %v", err) return } - scopes := strings.Split(scope, ",") - // Subject is empty since multiple VCs with different subjects can be provided expiration, signedToken, err := getApiVerifier().GenerateToken(clientId, "", clientId, scopes, presentation) if err != nil { logging.Log().Error("Failure during generating M2M token: ", err) - c.AbortWithStatusJSON(400, err) + c.AbortWithStatusJSON(http.StatusBadRequest, err) return } - response := TokenResponse{"Bearer", float32(expiration), signedToken, requestBody.Scope, ""} + response := TokenResponse{TokenType: "Bearer", IssuedTokenType: common.TYPE_ACCESS_TOKEN, ExpiresIn: float32(expiration), AccessToken: signedToken, Scope: strings.Join(scopes, ",")} logging.Log().Infof("Generated and signed token: %v", response) c.JSON(http.StatusOK, response) } @@ -179,7 +218,7 @@ func handleTokenTypeCode(c *gin.Context) { code, codeExists := c.GetPostForm("code") if !codeExists { logging.Log().Debug("No code present in the request.") - c.AbortWithStatusJSON(400, ErrorMessageNoCode) + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageNoCode) return } @@ -188,7 +227,7 @@ func handleTokenTypeCode(c *gin.Context) { if redirectUriExists { jwt, expiration, err := getApiVerifier().GetToken(code, redirectUri, false) if err != nil { - c.AbortWithStatusJSON(403, ErrorMessage{Summary: err.Error()}) + c.AbortWithStatusJSON(http.StatusForbidden, ErrorMessage{Summary: err.Error()}) return } c.JSON(http.StatusOK, TokenResponse{TokenType: "Bearer", ExpiresIn: float32(expiration), AccessToken: jwt}) @@ -198,13 +237,26 @@ func handleTokenTypeCode(c *gin.Context) { handleWithClientAssertion(c, assertionType, code) return } - c.AbortWithStatusJSON(400, ErrorMessageInvalidTokenRequest) + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageInvalidTokenRequest) +} + +func getScopesFromRequest(c *gin.Context) (scopes []string) { + + scope, scopeExists := c.GetPostForm("scope") + if !scopeExists { + logging.Log().Debug("No scope present in the request.") + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageNoScope) + return scopes + } + + return strings.Split(scope, ",") + } func handleWithClientAssertion(c *gin.Context, assertionType string, code string) { if assertionType != "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" { logging.Log().Warnf("Assertion type %s is not supported.", assertionType) - c.AbortWithStatusJSON(400, ErrorMessageUnsupportedAssertionType) + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageUnsupportedAssertionType) return } @@ -212,20 +264,20 @@ func handleWithClientAssertion(c *gin.Context, assertionType string, code string clientId, clientIdExists := c.GetPostForm("client_id") if !clientAssertionExists || !clientIdExists { logging.Log().Warnf("Client Id (%s) or assertion (%s) not provided.", clientId, clientAssertion) - c.AbortWithStatusJSON(400, ErrorMessageUnsupportedAssertionType) + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageUnsupportedAssertionType) return } kid, err := getKeyResolver().ExtractKIDFromJWT(clientAssertion) if err != nil { logging.Log().Warnf("Was not able to retrive kid from token %s. Err: %v.", clientAssertion, err) - c.AbortWithStatusJSON(400, ErrorMessageInvalidClientAssertion) + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageInvalidClientAssertion) return } pubKey, err := getKeyResolver().ResolvePublicKeyFromDID(kid) if err != nil { logging.Log().Warnf("Was not able to retrive key from kid %s. Err: %v.", kid, err) - c.AbortWithStatusJSON(400, ErrorMessageInvalidClientAssertion) + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageInvalidClientAssertion) return } @@ -238,7 +290,7 @@ func handleWithClientAssertion(c *gin.Context, assertionType string, code string parsed, err := jwt.Parse([]byte(clientAssertion), jwt.WithKey(alg, pubKey)) if err != nil { logging.Log().Warnf("Was not able to parse and verify the token %s. Err: %v", clientAssertion, err) - c.AbortWithStatusJSON(400, ErrorMessageInvalidClientAssertion) + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageInvalidClientAssertion) return } @@ -246,7 +298,7 @@ func handleWithClientAssertion(c *gin.Context, assertionType string, code string jsonBytes, err := json.Marshal(parsed) if err != nil { logging.Log().Warnf("Was not able to marshal the token %s. Err: %v", clientAssertion, err) - c.AbortWithStatusJSON(400, ErrorMessageInvalidClientAssertion) + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageInvalidClientAssertion) return } @@ -254,19 +306,19 @@ func handleWithClientAssertion(c *gin.Context, assertionType string, code string var clientAssertionObject ClientAssertion if err := json.Unmarshal(jsonBytes, &clientAssertionObject); err != nil { logging.Log().Warnf("Was not able to unmarshal the token: %s, Err: %v", string(jsonBytes), err) - c.AbortWithStatusJSON(400, ErrorMessageInvalidClientAssertion) + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageInvalidClientAssertion) return } if clientAssertionObject.Sub != clientId || clientAssertionObject.Iss != clientId || !slices.Contains(clientAssertionObject.Aud, getFrontendVerifier().GetHost()) { logging.Log().Warnf("Invalid assertion: %s. Client Id: %s, Host: %s", logging.PrettyPrintObject(clientAssertionObject), clientId, getApiVerifier().GetHost()) - c.AbortWithStatusJSON(400, ErrorMessageInvalidClientAssertion) + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageInvalidClientAssertion) return } jwt, expiration, err := getApiVerifier().GetToken(code, "", true) if err != nil { - c.AbortWithStatusJSON(403, ErrorMessage{Summary: err.Error()}) + c.AbortWithStatusJSON(http.StatusForbidden, ErrorMessage{Summary: err.Error()}) return } c.JSON(http.StatusOK, TokenResponse{TokenType: "Bearer", ExpiresIn: float32(expiration), AccessToken: jwt}) @@ -277,12 +329,13 @@ func StartSIOPSameDevice(c *gin.Context) { state, stateExists := c.GetQuery("state") if !stateExists { logging.Log().Debugf("No state was provided.") - c.AbortWithStatusJSON(400, ErrorMessage{"no_state_provided", "Authentication requires a state provided as query parameter."}) + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessage{"no_state_provided", "Authentication requires a state provided as query parameter."}) return } redirectPath, redirectPathExists := c.GetQuery("redirect_path") + requestProtocol := verifier.REDIRECT_PROTOCOL if !redirectPathExists { - redirectPath = "/" + requestProtocol = verifier.OPENID4VP_PROTOCOL } protocol := "https" @@ -302,13 +355,24 @@ func StartSIOPSameDevice(c *gin.Context) { requestMode = DEFAULT_REQUEST_MODE } - redirect, err := getApiVerifier().StartSameDeviceFlow(c.Request.Host, protocol, state, redirectPath, clientId, requestMode) + scope, scopeExists := c.GetQuery("scope") + if !scopeExists { + logging.Log().Infof("Start a login flow with default scope.") + scope = "" + } + + authenticationRequest, err := getApiVerifier().StartSameDeviceFlow(c.Request.Host, protocol, state, redirectPath, clientId, requestMode, scope, requestProtocol) if err != nil { logging.Log().Warnf("Error starting the same-device flow. Err: %v", err) - c.AbortWithStatusJSON(500, ErrorMessage{err.Error(), "Was not able to start the same device flow."}) + c.AbortWithStatusJSON(http.StatusInternalServerError, ErrorMessage{err.Error(), "Was not able to start the same device flow."}) return } - c.Redirect(302, redirect) + if requestProtocol == verifier.OPENID4VP_PROTOCOL { + c.String(http.StatusOK, authenticationRequest) + } else { + c.Redirect(http.StatusFound, authenticationRequest) + + } } // VerifierAPIAuthenticationResponse - Stores the credential for the given session @@ -318,7 +382,7 @@ func VerifierAPIAuthenticationResponse(c *gin.Context) { stateForm, stateFormExists := c.GetPostForm("state") stateQuery, stateQueryExists := c.GetQuery("state") if !stateFormExists && !stateQueryExists { - c.AbortWithStatusJSON(400, ErrorMessageNoState) + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageNoState) return } if stateFormExists { @@ -331,7 +395,7 @@ func VerifierAPIAuthenticationResponse(c *gin.Context) { vptoken, tokenExists := c.GetPostForm("vp_token") if !tokenExists { logging.Log().Info("No token was provided.") - c.AbortWithStatusJSON(400, ErrorMessageNoToken) + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageNoToken) return } @@ -347,14 +411,14 @@ func VerifierAPIAuthenticationResponse(c *gin.Context) { func GetVerifierAPIAuthenticationResponse(c *gin.Context) { state, stateExists := c.GetQuery("state") if !stateExists { - c.AbortWithStatusJSON(400, ErrorMessageNoState) + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageNoState) return } vpToken, tokenExists := c.GetQuery("vp_token") if !tokenExists { logging.Log().Info("No token was provided.") - c.AbortWithStatusJSON(400, ErrorMessageNoToken) + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageNoToken) return } presentation, err := extractVpFromToken(c, vpToken) @@ -372,7 +436,7 @@ func GetRequestByReference(c *gin.Context) { jwt, err := verifier.GetVerifier().GetRequestObject(sessionId) if err != nil { logging.Log().Debugf("No request for %s. Err: %v", sessionId, err) - c.AbortWithStatusJSON(404, ErrorMessageNoSuchSession) + c.AbortWithStatusJSON(http.StatusNotFound, ErrorMessageNoSuchSession) return } c.String(http.StatusOK, jwt) @@ -411,7 +475,7 @@ func tokenToPresentation(c *gin.Context, vpToken string) (parsedPresentation *ve if err != nil { logging.Log().Infof("Was not able to parse the token %s. Err: %v", vpToken, err) - c.AbortWithStatusJSON(400, ErrorMessageUnableToDecodeToken) + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageUnableToDecodeToken) return } return @@ -453,7 +517,7 @@ func isSdJWT(c *gin.Context, vpToken string) (isSdJwt bool, presentation *verifi vct, vct_ok := claims["vct"] if !i_ok || !vct_ok { logging.Log().Infof("Token does not contain issuer(%v) or vct(%v).", i_ok, vct_ok) - c.AbortWithStatusJSON(400, ErrorMessageInvalidSdJwt) + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageInvalidSdJwt) return true, presentation, errors.New(ErrorMessageInvalidSdJwt.Summary) } customFields := verifiable.CustomFields{} @@ -467,13 +531,13 @@ func isSdJWT(c *gin.Context, vpToken string) (isSdJwt bool, presentation *verifi credential, err := verifiable.CreateCredential(contents, verifiable.CustomFields{}) if err != nil { logging.Log().Infof("Was not able to create credential from sdJwt. E: %v", err) - c.AbortWithStatusJSON(400, ErrorMessageInvalidSdJwt) + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageInvalidSdJwt) return true, presentation, err } presentation, err = verifiable.NewPresentation() if err != nil { logging.Log().Infof("Was not able to create credpresentation from sdJwt. E: %v", err) - c.AbortWithStatusJSON(400, ErrorMessageInvalidSdJwt) + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageInvalidSdJwt) return true, presentation, err } presentation.AddCredentials(credential) @@ -495,7 +559,7 @@ func handleAuthenticationResponse(c *gin.Context, state string, presentation *ve response, err := getApiVerifier().AuthenticationResponse(state, presentation) if err != nil { logging.Log().Warnf("Was not able to fullfil the authentication response. Err: %v", err) - c.AbortWithStatusJSON(400, ErrorMessage{Summary: err.Error()}) + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessage{Summary: err.Error()}) return } if response != (verifier.Response{}) && response.FlowVersion == verifier.SAME_DEVICE { @@ -522,7 +586,7 @@ func VerifierAPIOpenIDConfiguration(c *gin.Context) { metadata, err := getApiVerifier().GetOpenIDConfiguration(c.Param("service_id")) if err != nil { - c.AbortWithStatusJSON(500, ErrorMessage{err.Error(), "Was not able to generate the OpenID metadata."}) + c.AbortWithStatusJSON(http.StatusInternalServerError, ErrorMessage{err.Error(), "Was not able to generate the OpenID metadata."}) return } c.JSON(http.StatusOK, metadata) @@ -532,14 +596,14 @@ func VerifierAPIOpenIDConfiguration(c *gin.Context) { func VerifierAPIStartSIOP(c *gin.Context) { state, stateExists := c.GetQuery("state") if !stateExists { - c.AbortWithStatusJSON(400, ErrorMessageNoState) + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageNoState) // early exit return } callback, callbackExists := c.GetQuery("client_callback") if !callbackExists { - c.AbortWithStatusJSON(400, ErrorMessageNoCallback) + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageNoCallback) // early exit return } @@ -560,7 +624,7 @@ func VerifierAPIStartSIOP(c *gin.Context) { connectionString, err := getApiVerifier().StartSiopFlow(c.Request.Host, protocol, callback, state, clientId, "", requestMode) if err != nil { - c.AbortWithStatusJSON(500, ErrorMessage{err.Error(), "Was not able to generate the connection string."}) + c.AbortWithStatusJSON(http.StatusInternalServerError, ErrorMessage{err.Error(), "Was not able to generate the connection string."}) return } c.String(http.StatusOK, connectionString) diff --git a/openapi/api_api_test.go b/openapi/api_api_test.go index c8f35d0..4d3ab58 100644 --- a/openapi/api_api_test.go +++ b/openapi/api_api_test.go @@ -43,7 +43,7 @@ func (mV *mockVerifier) ReturnLoginQRV2(host string, protocol string, callback s func (mV *mockVerifier) StartSiopFlow(host string, protocol string, callback string, sessionId string, clientId string, nonce string, requestType string) (connectionString string, err error) { return mV.mockConnectionString, mV.mockError } -func (mV *mockVerifier) StartSameDeviceFlow(host string, protocol string, sessionId string, redirectPath string, clientId string, requestType string) (authenticationRequest string, err error) { +func (mV *mockVerifier) StartSameDeviceFlow(host string, protocol string, sessionId string, redirectPath string, clientId string, requestType string, scope string, requestProtocol string) (authenticationRequest string, err error) { return mV.mockAuthRequest, mV.mockError } func (mV *mockVerifier) GetToken(authorizationCode string, redirectUri string, validated bool) (jwtString string, expiration int64, err error) { @@ -76,19 +76,22 @@ func TestGetToken(t *testing.T) { logging.Configure(true, "DEBUG", true, []string{}) type test struct { - testName string - proofCheck bool - testGrantType string - testCode string - testRedirectUri string - testVPToken string - testScope string - mockJWTString string - mockExpiration int64 - mockError error - expectedStatusCode int - expectedResponse TokenResponse - expectedError ErrorMessage + testName string + proofCheck bool + testGrantType string + testCode string + testRedirectUri string + testVPToken string + testScope string + testResource string + testSubjectTokenType string + testRequestedTokenType string + mockJWTString string + mockExpiration int64 + mockError error + expectedStatusCode int + expectedResponse TokenResponse + expectedError ErrorMessage } tests := []test{ {testName: "If a valid authorization_code request is received a token should be responded.", proofCheck: false, testGrantType: "authorization_code", testCode: "my-auth-code", testRedirectUri: "http://my-redirect.org", mockJWTString: "theJWT", mockExpiration: 10, mockError: nil, expectedStatusCode: 200, expectedResponse: TokenResponse{TokenType: "Bearer", ExpiresIn: 10, AccessToken: "theJWT"}, expectedError: ErrorMessage{}}, @@ -98,10 +101,17 @@ func TestGetToken(t *testing.T) { {testName: "If no redirect uri is provided, the request should fail.", proofCheck: false, testGrantType: "authorization_code", testCode: "my-auth-code", expectedStatusCode: 400, expectedError: ErrorMessageInvalidTokenRequest}, {testName: "If the verify returns an error, a 403 should be answerd.", proofCheck: false, testGrantType: "authorization_code", testCode: "my-auth-code", testRedirectUri: "http://my-redirect.org", mockError: errors.New("invalid"), expectedStatusCode: 403, expectedError: ErrorMessage{}}, - {testName: "If a valid vp_token request is received a token should be responded.", proofCheck: false, testGrantType: "vp_token", testVPToken: getValidVPToken(), testScope: "tir_read", mockJWTString: "theJWT", mockExpiration: 10, expectedStatusCode: 200, expectedResponse: TokenResponse{TokenType: "Bearer", ExpiresIn: 10, AccessToken: "theJWT"}}, - {testName: "If a valid signed vp_token request is received a token should be responded.", proofCheck: true, testGrantType: "vp_token", testVPToken: getValidSignedDidKeyVPToken(), testScope: "tir_read", mockJWTString: "theJWT", mockExpiration: 10, expectedStatusCode: 200, expectedResponse: TokenResponse{TokenType: "Bearer", ExpiresIn: 10, AccessToken: "theJWT"}}, + {testName: "If a valid vp_token request is received a token should be responded.", proofCheck: false, testGrantType: "vp_token", testVPToken: getValidVPToken(), testScope: "tir_read", mockJWTString: "theJWT", mockExpiration: 10, expectedStatusCode: 200, expectedResponse: TokenResponse{TokenType: "Bearer", ExpiresIn: 10, AccessToken: "theJWT", Scope: "tir_read", IssuedTokenType: common.TYPE_ACCESS_TOKEN}}, + {testName: "If a valid signed vp_token request is received a token should be responded.", proofCheck: true, testGrantType: "vp_token", testVPToken: getValidSignedDidKeyVPToken(), testScope: "tir_read", mockJWTString: "theJWT", mockExpiration: 10, expectedStatusCode: 200, expectedResponse: TokenResponse{TokenType: "Bearer", ExpiresIn: 10, AccessToken: "theJWT", Scope: "tir_read", IssuedTokenType: common.TYPE_ACCESS_TOKEN}}, {testName: "If no valid vp_token is provided, the request should fail.", proofCheck: false, testGrantType: "vp_token", testScope: "tir_read", expectedStatusCode: 400, expectedError: ErrorMessageNoToken}, {testName: "If no valid scope is provided, the request should fail.", proofCheck: false, testVPToken: getValidVPToken(), testGrantType: "vp_token", expectedStatusCode: 400, expectedError: ErrorMessageNoScope}, + // token-exchange + {testName: "If a valid token-exchange request is received a token should be responded.", proofCheck: false, testGrantType: "urn:ietf:params:oauth:grant-type:token-exchange", testVPToken: getValidVPToken(), testScope: "tir_read", testResource: "my-client-id", testSubjectTokenType: "urn:eu:oidf:vp_token", mockJWTString: "theJWT", mockExpiration: 10, expectedStatusCode: 200, expectedResponse: TokenResponse{TokenType: "Bearer", ExpiresIn: 10, AccessToken: "theJWT", Scope: "tir_read", IssuedTokenType: common.TYPE_ACCESS_TOKEN}}, + {testName: "If a token-exchange request is received without resource, it should fail.", proofCheck: false, testGrantType: "urn:ietf:params:oauth:grant-type:token-exchange", testVPToken: getValidVPToken(), testScope: "tir_read", testSubjectTokenType: "urn:eu:oidf:vp_token", expectedStatusCode: 400, expectedError: ErrorMessageNoResource}, + {testName: "If a token-exchange request is received with invalid subject_token_type, it should fail.", proofCheck: false, testGrantType: "urn:ietf:params:oauth:grant-type:token-exchange", testVPToken: getValidVPToken(), testScope: "tir_read", testResource: "my-client-id", testSubjectTokenType: "invalid_type", expectedStatusCode: 400, expectedError: ErrorMessageInvalidSubjectTokenType}, + {testName: "If a token-exchange request is received with invalid requested_token_type, it should fail.", proofCheck: false, testGrantType: "urn:ietf:params:oauth:grant-type:token-exchange", testVPToken: getValidVPToken(), testScope: "tir_read", testResource: "my-client-id", testSubjectTokenType: "urn:eu:oidf:vp_token", testRequestedTokenType: "invalid_type", expectedStatusCode: 400, expectedError: ErrorMessageInvalidRequestedTokenType}, + {testName: "If a token-exchange request is received without subject_token, it should fail.", proofCheck: false, testGrantType: "urn:ietf:params:oauth:grant-type:token-exchange", testScope: "tir_read", testResource: "my-client-id", testSubjectTokenType: "urn:eu:oidf:vp_token", expectedStatusCode: 400, expectedError: ErrorMessageNoToken}, + {testName: "If a token-exchange request is received without scope, it should fail.", proofCheck: false, testGrantType: "urn:ietf:params:oauth:grant-type:token-exchange", testVPToken: getValidVPToken(), testResource: "my-client-id", testSubjectTokenType: "urn:eu:oidf:vp_token", expectedStatusCode: 400, expectedError: ErrorMessageNoScope}, } for _, tc := range tests { @@ -143,7 +153,21 @@ func TestGetToken(t *testing.T) { } if tc.testVPToken != "" { - formArray = append(formArray, "vp_token="+tc.testVPToken) + if tc.testGrantType == "vp_token" { + formArray = append(formArray, "vp_token="+tc.testVPToken) + } else if tc.testGrantType == "urn:ietf:params:oauth:grant-type:token-exchange" { + formArray = append(formArray, "subject_token="+tc.testVPToken) + } + } + + if tc.testResource != "" { + formArray = append(formArray, "resource="+tc.testResource) + } + if tc.testSubjectTokenType != "" { + formArray = append(formArray, "subject_token_type="+tc.testSubjectTokenType) + } + if tc.testRequestedTokenType != "" { + formArray = append(formArray, "requested_token_type="+tc.testRequestedTokenType) } body := bytes.NewBufferString(strings.Join(formArray, "&")) @@ -199,13 +223,14 @@ func TestStartSIOPSameDevice(t *testing.T) { mockError error expectedStatusCode int expectedLocation string + expectedResponse string } tests := []test{ - {"If all neccessary parameters provided, a valid redirect should be returned.", "my-state", "/my-redirect", "http://host.org", "http://host.org/api/v1/authentication_response", nil, 302, "http://host.org/api/v1/authentication_response"}, - {"If no path is provided, the default redirect should be returned.", "my-state", "", "http://host.org", "http://host.org/api/v1/authentication_response", nil, 302, "http://host.org/api/v1/authentication_response"}, - {"If no state is provided, a 400 should be returned.", "", "", "http://host.org", "http://host.org/api/v1/authentication_response", nil, 400, ""}, - {"If the verifier returns an error, a 500 should be returned.", "my-state", "/", "http://host.org", "http://host.org/api/v1/authentication_response", errors.New("verifier_failure"), 500, ""}, + {testName: "If all neccessary parameters provided, a valid redirect should be returned.", testState: "my-state", testRedirectPath: "/my-redirect", testRequestAddress: "http://host.org", mockRedirect: "http://host.org/api/v1/authentication_response", mockError: nil, expectedStatusCode: 302, expectedLocation: "http://host.org/api/v1/authentication_response"}, + {testName: "If no state is provided, a 400 should be returned.", testState: "", testRedirectPath: "", testRequestAddress: "http://host.org", mockRedirect: "http://host.org/api/v1/authentication_response", mockError: nil, expectedStatusCode: 400, expectedLocation: ""}, + {testName: "If the verifier returns an error, a 500 should be returned.", testState: "my-state", testRedirectPath: "/", testRequestAddress: "http://host.org", mockRedirect: "http://host.org/api/v1/authentication_response", mockError: errors.New("verifier_failure"), expectedStatusCode: 500, expectedLocation: ""}, + {testName: "If no path is provided, a deeplink should be returned.", testState: "my-state", testRedirectPath: "", testRequestAddress: "http://host.org", mockRedirect: "http://host.org/api/v1/authentication_response", mockError: nil, expectedStatusCode: 200, expectedLocation: "", expectedResponse: "http://host.org/api/v1/authentication_response"}, } for _, tc := range tests { @@ -233,6 +258,13 @@ func TestStartSIOPSameDevice(t *testing.T) { t.Errorf("%s - Expected status %v but was %v.", tc.testName, tc.expectedStatusCode, recorder.Code) return } + if tc.expectedStatusCode == 200 { + responseString := recorder.Body.String() + if tc.expectedResponse != responseString { + t.Errorf("%s - Expected response %v but was %v.", tc.testName, tc.expectedResponse, responseString) + } + } + if tc.expectedStatusCode != 302 { // everything other is an error, we dont care about the details return diff --git a/openapi/api_frontend.go b/openapi/api_frontend.go index f822c0b..50550fe 100644 --- a/openapi/api_frontend.go +++ b/openapi/api_frontend.go @@ -43,14 +43,14 @@ func VerifierPageDisplayQRSIOP(c *gin.Context) { state, stateExists := c.GetQuery("state") if !stateExists { - c.AbortWithStatusJSON(400, ErrorMessageNoState) + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageNoState) // early exit return } callback, callbackExists := c.GetQuery("client_callback") if !callbackExists { - c.AbortWithStatusJSON(400, ErrorMessageNoCallback) + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageNoCallback) // early exit return } @@ -73,7 +73,7 @@ func VerifierPageDisplayQRSIOP(c *gin.Context) { qr, err := getFrontendVerifier().ReturnLoginQR(c.Request.Host, "https", callback, state, clientId, nonce, requestMode) if err != nil { - c.AbortWithStatusJSON(500, ErrorMessage{"qr_generation_error", err.Error()}) + c.AbortWithStatusJSON(http.StatusInternalServerError, ErrorMessage{"qr_generation_error", err.Error()}) return } @@ -85,7 +85,7 @@ func VerifierLoginQr(c *gin.Context) { state, stateExists := c.GetQuery("state") if !stateExists { - c.AbortWithStatusJSON(400, ErrorMessageNoState) + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageNoState) // early exit return } @@ -94,7 +94,7 @@ func VerifierLoginQr(c *gin.Context) { requestUri, requestUriExists := c.GetQuery("request_uri") if !redirectUriExists && !requestUriExists { - c.AbortWithStatusJSON(400, ErrorMessageNoRedircetUri) + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageNoRedircetUri) // early exit return } @@ -115,11 +115,11 @@ func VerifierLoginQr(c *gin.Context) { cro, err := getRequestObjectClient().GetClientRequestObject(requestUri) if err != nil { logging.Log().Warnf("Was not able to get request object. Err: %v", err) - c.AbortWithStatusJSON(500, ErrorMessageUnresolvableRequestObject) + c.AbortWithStatusJSON(http.StatusInternalServerError, ErrorMessageUnresolvableRequestObject) return } if !slices.Contains(cro.Aud, getFrontendVerifier().GetHost()) { - c.AbortWithStatusJSON(500, ErrorMessageInvalidAudience) + c.AbortWithStatusJSON(http.StatusInternalServerError, ErrorMessageInvalidAudience) return } @@ -130,7 +130,7 @@ func VerifierLoginQr(c *gin.Context) { nonce, nonceExists := c.GetQuery("nonce") if !nonceExists { - c.AbortWithStatusJSON(400, ErrorMessageNoNonce) + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorMessageNoNonce) // early exit return } diff --git a/openapi/model_token_response.go b/openapi/model_token_response.go index 3367475..037d165 100644 --- a/openapi/model_token_response.go +++ b/openapi/model_token_response.go @@ -10,7 +10,8 @@ package openapi type TokenResponse struct { - TokenType string `json:"token_type,omitempty"` + TokenType string `json:"token_type,omitempty"` + IssuedTokenType string `json:"issued_token_type,omitempty"` // The lifetime in seconds of the access token ExpiresIn float32 `json:"expires_in,omitempty"` diff --git a/verifier/credentialsConfig.go b/verifier/credentialsConfig.go index a19f310..f08c7e5 100644 --- a/verifier/credentialsConfig.go +++ b/verifier/credentialsConfig.go @@ -24,6 +24,8 @@ const CACHE_EXPIRY = 60 type CredentialsConfig interface { // should return the list of scopes to be requested via the scope parameter GetScope(serviceIdentifier string) (scopes []string, err error) + // should return the authorization path to be provided as part of the oid-metadata + GetAuthorizationPath(serviceIdentifier string) (path string, err error) // should return the presentationDefinition be requested via the scope parameter GetPresentationDefinition(serviceIdentifier string, scope string) (presentationDefinition *config.PresentationDefinition) // should return the presentatiodcql to be requested via the scope parameter @@ -143,6 +145,17 @@ func (cc ServiceBackedCredentialsConfig) GetScope(serviceIdentifier string) (sco return []string{}, nil } +func (cc ServiceBackedCredentialsConfig) GetAuthorizationPath(serviceIdentifier string) (path string, err error) { + cacheEntry, hit := common.GlobalCache.ServiceCache.Get(serviceIdentifier) + if hit { + logging.Log().Debugf("Found authorization-path for %s", serviceIdentifier) + configuredService := cacheEntry.(config.ConfiguredService) + return configuredService.AuthorizationPath, nil + } + logging.Log().Debugf("No authorization-path entry for %s", serviceIdentifier) + return "", nil +} + func (cc ServiceBackedCredentialsConfig) GetPresentationDefinition(serviceIdentifier string, scope string) (presentationDefinition *config.PresentationDefinition) { cacheEntry, hit := common.GlobalCache.ServiceCache.Get(serviceIdentifier) if hit { diff --git a/verifier/verifier.go b/verifier/verifier.go index 88359b0..dba4ec0 100644 --- a/verifier/verifier.go +++ b/verifier/verifier.go @@ -48,6 +48,11 @@ const ( SAME_DEVICE ) +const OPENID4VP_PROTOCOL = "openid4vp" +const REDIRECT_PROTOCOL = "redirect" + +const DEFAULT_AUTHORIZATION_PATH = "/api/v2/loginQR" + var ErrorNoDID = errors.New("no_did_configured") var ErrorNoTIR = errors.New("no_tir_configured") var ErrorUnsupportedKeyAlgorithm = errors.New("unsupported_key_algorithm") @@ -75,8 +80,8 @@ var ErrorInvalidNonce = errors.New("invalid_nonce") type Verifier interface { ReturnLoginQR(host string, protocol string, callback string, sessionId string, clientId string, nonce string, requestMode string) (qr string, err error) ReturnLoginQRV2(host string, protocol string, callback string, sessionId string, clientId string, scope string, nonce string, requestMode string) (qr string, err error) - StartSiopFlow(host string, protocol string, callback string, sessionId string, clientId string, nonce string, requestMode string) (connectionString string, err error) - StartSameDeviceFlow(host string, protocol string, sessionId string, redirectPath string, clientId string, requestMode string) (authenticationRequest string, err error) + StartSiopFlow(host string, protocol string, callback string, state string, clientId string, nonce string, requestMode string) (connectionString string, err error) + StartSameDeviceFlow(host string, protocol string, sessionId string, redirectPath string, clientId string, requestMode string, scope string, requestProtocol string) (authenticationRequest string, err error) GetToken(authorizationCode string, redirectUri string, validated bool) (jwtString string, expiration int64, err error) GetJWKS() jwk.Set AuthenticationResponse(state string, verifiablePresentation *verifiable.Presentation) (sameDevice Response, err error) @@ -397,29 +402,32 @@ func (v *CredentialVerifier) ReturnLoginQRV2(host string, protocol string, redir /** * Starts a siop-flow and returns the required connection information **/ -func (v *CredentialVerifier) StartSiopFlow(host string, protocol string, callback string, sessionId string, clientId string, nonce string, requestMode string) (connectionString string, err error) { +func (v *CredentialVerifier) StartSiopFlow(host string, protocol string, callback string, state string, clientId string, nonce string, requestMode string) (connectionString string, err error) { logging.Log().Debugf("Start a plain siop-flow for %s.", callback) - return v.initSiopFlow(host, protocol, callback, sessionId, clientId, nonce, requestMode) + return v.initSiopFlow(host, protocol, callback, state, clientId, nonce, requestMode) } /** * Starts a same-device siop-flow and returns the required redirection information **/ -func (v *CredentialVerifier) StartSameDeviceFlow(host string, protocol string, sessionId string, redirectPath string, clientId string, requestMode string) (authenticationRequest string, err error) { +func (v *CredentialVerifier) StartSameDeviceFlow(host string, protocol string, state string, redirectPath string, clientId string, requestMode string, scope string, requestProtocol string) (authenticationRequest string, err error) { logging.Log().Debugf("Initiate samedevice flow for %s - %s.", host, clientId) - state := v.nonceGenerator.GenerateNonce() + nonce := v.nonceGenerator.GenerateNonce() - loginSession := loginSession{fmt.Sprintf("%s://%s%s", protocol, host, redirectPath), sessionId, "", clientId, "", SAME_DEVICE} + loginSession := loginSession{callback: fmt.Sprintf("%s://%s%s", protocol, host, redirectPath), sessionId: state, nonce: nonce, clientId: clientId, version: SAME_DEVICE} err = v.sessionCache.Add(state, loginSession, cache.DefaultExpiration) if err != nil { logging.Log().Warnf("Was not able to store the login session %s in cache. Err: %v", logging.PrettyPrintObject(loginSession), err) return authenticationRequest, err } - redirectUri := fmt.Sprintf("%s://%s/api/v1/authentication_response", protocol, host) - - return v.generateAuthenticationRequest(protocol+"://"+host+redirectPath, clientId, "", redirectUri, state, "", loginSession, requestMode) + authResponseUri := fmt.Sprintf("%s://%s/api/v1/authentication_response", protocol, host) + if requestProtocol == OPENID4VP_PROTOCOL { + return v.generateAuthenticationRequest(requestProtocol+"://", clientId, scope, authResponseUri, state, nonce, loginSession, requestMode) + } else { + return v.generateAuthenticationRequest(protocol+"://"+host+redirectPath, clientId, scope, authResponseUri, state, nonce, loginSession, requestMode) + } } /** @@ -695,12 +703,24 @@ func (v *CredentialVerifier) GetOpenIDConfiguration(serviceIdentifier string) (m return metadata, err } + authorizationPath, err := v.credentialsConfig.GetAuthorizationPath(serviceIdentifier) + if err != nil { + return metadata, err + } + if authorizationPath == "" && v.verifierConfig.AuthorizationEndpoint == "" { + // static default in case nothing is provided + authorizationPath = DEFAULT_AUTHORIZATION_PATH + } else if authorizationPath == "" { + // configured default + authorizationPath = v.verifierConfig.AuthorizationEndpoint + } + return common.OpenIDProviderMetadata{ Issuer: v.host, - AuthorizationEndpoint: v.host + v.verifierConfig.AuthorizationEndpoint, + AuthorizationEndpoint: appendPath(v.host, authorizationPath), TokenEndpoint: v.host + "/services/" + serviceIdentifier + "/token", JwksUri: v.host + "/.well-known/jwks", - GrantTypesSupported: []string{"authorization_code", "vp_token"}, + GrantTypesSupported: []string{"authorization_code", "vp_token", "urn:ietf:params:oauth:grant-type:token-exchange"}, ResponseTypesSupported: []string{"token"}, ResponseModeSupported: []string{"direct_post"}, SubjectTypesSupported: []string{"public"}, @@ -708,6 +728,12 @@ func (v *CredentialVerifier) GetOpenIDConfiguration(serviceIdentifier string) (m ScopesSupported: scopes}, err } +func appendPath(host string, path string) string { + host = strings.TrimSuffix(host, "/") + path = strings.TrimPrefix(path, "/") + return host + "/" + path +} + /** * Receive credentials and verify them in the context of an already present login-session. Will return either an error if failed, a sameDevice response to be used for * redirection or notify the original initiator(in case of a cross-device flow) @@ -991,10 +1017,12 @@ func (v *CredentialVerifier) initOid4VPCrossDevice(host string, protocol string, } // initializes the cross-device siop flow -func (v *CredentialVerifier) initSiopFlow(host string, protocol string, callback string, sessionId string, clientId string, nonce string, requestMode string) (authenticationRequest string, err error) { +func (v *CredentialVerifier) initSiopFlow(host string, protocol string, callback string, state string, clientId string, nonce string, requestMode string) (authenticationRequest string, err error) { - state := v.nonceGenerator.GenerateNonce() - loginSession := loginSession{callback, sessionId, nonce, clientId, "", CROSS_DEVICE_V1} + if nonce == "" { + nonce = v.nonceGenerator.GenerateNonce() + } + loginSession := loginSession{callback, state, nonce, clientId, "", CROSS_DEVICE_V1} err = v.sessionCache.Add(state, loginSession, cache.DefaultExpiration) if err != nil { @@ -1090,13 +1118,12 @@ func (v *CredentialVerifier) createAuthenticationRequestByReference(base string, } func (v *CredentialVerifier) createAuthenticationRequestObject(response_uri string, state string, clientId string, scope string, nonce string) (requestObject []byte, err error) { - jwtBuilder := jwt.NewBuilder().Issuer(v.did) + jwtBuilder := jwt.NewBuilder().Issuer(v.clientIdentification.Id) jwtBuilder.Claim("response_type", "vp_token") jwtBuilder.Claim("response_mode", "direct_post") jwtBuilder.Claim("client_id", v.clientIdentification.Id) jwtBuilder.Claim("response_uri", response_uri) jwtBuilder.Claim("state", state) - jwtBuilder.Claim("scope", "openid") if nonce != "" { jwtBuilder.Claim("nonce", nonce) } diff --git a/verifier/verifier_test.go b/verifier/verifier_test.go index 88818c8..854a9a9 100644 --- a/verifier/verifier_test.go +++ b/verifier/verifier_test.go @@ -113,6 +113,12 @@ func (mcc mockCredentialConfig) GetScope(serviceIdentifier string) (scopes []str return maps.Keys(mcc.mockScopes[serviceIdentifier]), err } +func (mcc mockCredentialConfig) GetAuthorizationPath(serviceIdentifier string) (path string, err error) { + if mcc.mockError != nil { + return path, mcc.mockError + } + return DEFAULT_AUTHORIZATION_PATH, err +} func (mcc mockCredentialConfig) GetPresentationDefinition(serviceIdentifier string, scope string) (presentationDefinition *configModel.PresentationDefinition) { if mcc.mockError != nil { return presentationDefinition @@ -256,10 +262,12 @@ type siopInitTest struct { testHost string testProtocol string testAddress string - testSessionId string + testState string testClientId string testRequestObjectJwt string testNonce string + testScope string + testRequestProtocol string requestMode string credentialScopes map[string]map[string]configModel.ScopeEntry mockConfigError error @@ -269,6 +277,29 @@ type siopInitTest struct { expectedError error } +func getInitSiopTests() []siopInitTest { + + cacheFailError := errors.New("cache_fail") + + return []siopInitTest{ + {testName: "If the login-session could not be cached, an error should be thrown.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testState: "my-super-random-id", testClientId: "", requestMode: REQUEST_MODE_BY_VALUE, credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://client.org/callback", + expectedConnection: "", sessionCacheError: cacheFailError, expectedError: cacheFailError, + }, + {testName: "If all parameters are set, a proper connection string byValue should be returned.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testState: "my-super-random-id", testClientId: "", requestMode: REQUEST_MODE_BY_VALUE, credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://client.org/callback", + expectedConnection: "openid4vp://?client_id=did:key:verifier&request=eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ.eyJjbGllbnRfaWQiOiJkaWQ6a2V5OnZlcmlmaWVyIiwiZXhwIjozMCwiaXNzIjoiZGlkOmtleTp2ZXJpZmllciIsIm5vbmNlIjoicmFuZG9tTm9uY2UiLCJyZXNwb25zZV9tb2RlIjoiZGlyZWN0X3Bvc3QiLCJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJyZXNwb25zZV91cmkiOiJodHRwczovL3ZlcmlmaWVyLm9yZy9hcGkvdjEvYXV0aGVudGljYXRpb25fcmVzcG9uc2UiLCJzdGF0ZSI6Im15LXN1cGVyLXJhbmRvbS1pZCJ9", sessionCacheError: nil, expectedError: nil, + }, + {testName: "If all parameters are set, a proper connection string byReference should be returned.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testState: "my-super-random-id", testClientId: "", requestMode: REQUEST_MODE_BY_REFERENCE, credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://client.org/callback", + expectedConnection: "openid4vp://?client_id=did:key:verifier&request_uri=verifier.org/api/v1/request/my-super-random-id&request_uri_method=get", sessionCacheError: nil, expectedError: nil, testRequestObjectJwt: "eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ.eyJjbGllbnRfaWQiOiJkaWQ6a2V5OnZlcmlmaWVyIiwiZXhwIjozMCwiaXNzIjoiZGlkOmtleTp2ZXJpZmllciIsIm5vbmNlIjoicmFuZG9tTm9uY2UiLCJyZXNwb25zZV9tb2RlIjoiZGlyZWN0X3Bvc3QiLCJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJyZXNwb25zZV91cmkiOiJodHRwczovL3ZlcmlmaWVyLm9yZy9hcGkvdjEvYXV0aGVudGljYXRpb25fcmVzcG9uc2UiLCJzdGF0ZSI6Im15LXN1cGVyLXJhbmRvbS1pZCJ9", + }, + {testName: "If all parameters, including the nonce, are set, a proper connection string byValue should be returned.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testState: "my-super-random-id", testClientId: "", testNonce: "my-nonce", requestMode: REQUEST_MODE_BY_VALUE, credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://client.org/callback", + expectedConnection: "openid4vp://?client_id=did:key:verifier&request=eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ.eyJjbGllbnRfaWQiOiJkaWQ6a2V5OnZlcmlmaWVyIiwiZXhwIjozMCwiaXNzIjoiZGlkOmtleTp2ZXJpZmllciIsIm5vbmNlIjoibXktbm9uY2UiLCJyZXNwb25zZV9tb2RlIjoiZGlyZWN0X3Bvc3QiLCJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJyZXNwb25zZV91cmkiOiJodHRwczovL3ZlcmlmaWVyLm9yZy9hcGkvdjEvYXV0aGVudGljYXRpb25fcmVzcG9uc2UiLCJzdGF0ZSI6Im15LXN1cGVyLXJhbmRvbS1pZCJ9", sessionCacheError: nil, expectedError: nil, + }, + {testName: "If all parameters are set, including the nonce, a proper connection string byReference should be returned.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testState: "my-super-random-id", testClientId: "", testNonce: "my-nonce", requestMode: REQUEST_MODE_BY_REFERENCE, credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://client.org/callback", + expectedConnection: "openid4vp://?client_id=did:key:verifier&request_uri=verifier.org/api/v1/request/my-super-random-id&request_uri_method=get", sessionCacheError: nil, expectedError: nil, testRequestObjectJwt: "eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ.eyJjbGllbnRfaWQiOiJkaWQ6a2V5OnZlcmlmaWVyIiwiZXhwIjozMCwiaXNzIjoiZGlkOmtleTp2ZXJpZmllciIsIm5vbmNlIjoibXktbm9uY2UiLCJyZXNwb25zZV9tb2RlIjoiZGlyZWN0X3Bvc3QiLCJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJyZXNwb25zZV91cmkiOiJodHRwczovL3ZlcmlmaWVyLm9yZy9hcGkvdjEvYXV0aGVudGljYXRpb25fcmVzcG9uc2UiLCJzdGF0ZSI6Im15LXN1cGVyLXJhbmRvbS1pZCJ9", + }, + } +} + func TestInitSiopFlow(t *testing.T) { logging.Configure(true, "DEBUG", true, []string{}) @@ -280,10 +311,10 @@ func TestInitSiopFlow(t *testing.T) { t.Run(tc.testName, func(t *testing.T) { logging.Log().Info("TestInitSiopFlow +++++++++++++++++ Running test: ", tc.testName) sessionCache := mockSessionCache{sessions: map[string]loginSession{}, errorToThrow: tc.sessionCacheError} - nonceGenerator := mockNonceGenerator{staticValues: []string{"randomState", "randomNonce"}} + nonceGenerator := mockNonceGenerator{staticValues: []string{"randomNonce"}} credentialsConfig := mockCredentialConfig{tc.credentialScopes, tc.mockConfigError} verifier := CredentialVerifier{host: tc.testHost, did: "did:key:verifier", sessionCache: &sessionCache, nonceGenerator: &nonceGenerator, tokenSigner: mockTokenSigner{}, clock: mockClock{}, credentialsConfig: credentialsConfig, requestSigningKey: &testKey, clientIdentification: configModel.ClientIdentification{Id: "did:key:verifier", KeyPath: "/my-signing-key.pem", KeyAlgorithm: "ES256"}} - authReq, err := verifier.initSiopFlow(tc.testHost, tc.testProtocol, tc.testAddress, tc.testSessionId, tc.testClientId, tc.testNonce, tc.requestMode) + authReq, err := verifier.initSiopFlow(tc.testHost, tc.testProtocol, tc.testAddress, tc.testState, tc.testClientId, tc.testNonce, tc.requestMode) verifyInitTest(t, tc, authReq, err, sessionCache, CROSS_DEVICE_V1) }) } @@ -301,10 +332,10 @@ func TestStartSiopFlow(t *testing.T) { t.Run(tc.testName, func(t *testing.T) { logging.Log().Info("TestStartSiopFlow +++++++++++++++++ Running test: ", tc.testName) sessionCache := mockSessionCache{sessions: map[string]loginSession{}, errorToThrow: tc.sessionCacheError} - nonceGenerator := mockNonceGenerator{staticValues: []string{"randomState", "randomNonce"}} + nonceGenerator := mockNonceGenerator{staticValues: []string{"randomNonce"}} credentialsConfig := mockCredentialConfig{tc.credentialScopes, tc.mockConfigError} verifier := CredentialVerifier{host: tc.testHost, did: "did:key:verifier", sessionCache: &sessionCache, nonceGenerator: &nonceGenerator, tokenSigner: mockTokenSigner{}, clock: mockClock{}, requestSigningKey: &testKey, credentialsConfig: credentialsConfig, clientIdentification: configModel.ClientIdentification{Id: "did:key:verifier", KeyPath: "/my-signing-key.pem", KeyAlgorithm: "ES256"}} - authReq, err := verifier.StartSiopFlow(tc.testHost, tc.testProtocol, tc.testAddress, tc.testSessionId, tc.testClientId, tc.testNonce, tc.requestMode) + authReq, err := verifier.StartSiopFlow(tc.testHost, tc.testProtocol, tc.testAddress, tc.testState, tc.testClientId, tc.testNonce, tc.requestMode) verifyInitTest(t, tc, authReq, err, sessionCache, CROSS_DEVICE_V1) }) } @@ -331,16 +362,21 @@ func verifyInitTest(t *testing.T, tc siopInitTest, authRequest string, err error if authRequest != tc.expectedConnection && tc.requestMode != REQUEST_MODE_BY_VALUE { t.Errorf("%s - Expected %s but was %s", tc.testName, tc.expectedConnection, authRequest) } - cachedSession, found := sessionCache.sessions["randomState"] + cachedSession, found := sessionCache.sessions[tc.testState] if !found { t.Errorf("%s - A login session should have been stored.", tc.testName) } var expectedSession loginSession + + expectedNonce := tc.testNonce + if expectedNonce == "" { + expectedNonce = "randomNonce" + } if tc.requestMode == REQUEST_MODE_BY_REFERENCE { - expectedSession = loginSession{version: flowVersion, callback: tc.expectedCallback, nonce: tc.testNonce, sessionId: tc.testSessionId, clientId: tc.testClientId, requestObject: tc.testRequestObjectJwt} + expectedSession = loginSession{version: flowVersion, callback: tc.expectedCallback, nonce: expectedNonce, sessionId: tc.testState, clientId: tc.testClientId, requestObject: tc.testRequestObjectJwt} cachedSession.requestObject = removeSignature(cachedSession.requestObject) } else { - expectedSession = loginSession{version: flowVersion, callback: tc.expectedCallback, nonce: tc.testNonce, sessionId: tc.testSessionId, clientId: tc.testClientId, requestObject: tc.testRequestObjectJwt} + expectedSession = loginSession{version: flowVersion, callback: tc.expectedCallback, nonce: expectedNonce, sessionId: tc.testState, clientId: tc.testClientId, requestObject: tc.testRequestObjectJwt} } if cachedSession != expectedSession { t.Errorf("%s - The login session was expected to be %v but was %v.", tc.testName, expectedSession, cachedSession) @@ -353,29 +389,6 @@ func removeSignature(jwt string) string { return strings.Join(splitted, ".") } -func getInitSiopTests() []siopInitTest { - - cacheFailError := errors.New("cache_fail") - - return []siopInitTest{ - {testName: "If the login-session could not be cached, an error should be thrown.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testSessionId: "my-super-random-id", testClientId: "", requestMode: REQUEST_MODE_BY_VALUE, credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://client.org/callback", - expectedConnection: "", sessionCacheError: cacheFailError, expectedError: cacheFailError, - }, - {testName: "If all parameters are set, a proper connection string byValue should be returned.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testSessionId: "my-super-random-id", testClientId: "", requestMode: REQUEST_MODE_BY_VALUE, credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://client.org/callback", - expectedConnection: "openid4vp://?client_id=did:key:verifier&request=eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ.eyJjbGllbnRfaWQiOiJkaWQ6a2V5OnZlcmlmaWVyIiwiZXhwIjozMCwiaXNzIjoiZGlkOmtleTp2ZXJpZmllciIsInJlc3BvbnNlX21vZGUiOiJkaXJlY3RfcG9zdCIsInJlc3BvbnNlX3R5cGUiOiJ2cF90b2tlbiIsInJlc3BvbnNlX3VyaSI6Imh0dHBzOi8vdmVyaWZpZXIub3JnL2FwaS92MS9hdXRoZW50aWNhdGlvbl9yZXNwb25zZSIsInNjb3BlIjoib3BlbmlkIiwic3RhdGUiOiJyYW5kb21TdGF0ZSJ9", sessionCacheError: nil, expectedError: nil, - }, - {testName: "If all parameters are set, a proper connection string byReference should be returned.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testSessionId: "my-super-random-id", testClientId: "", requestMode: REQUEST_MODE_BY_REFERENCE, credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://client.org/callback", - expectedConnection: "openid4vp://?client_id=did:key:verifier&request_uri=verifier.org/api/v1/request/randomState&request_uri_method=get", sessionCacheError: nil, expectedError: nil, testRequestObjectJwt: "eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ.eyJjbGllbnRfaWQiOiJkaWQ6a2V5OnZlcmlmaWVyIiwiZXhwIjozMCwiaXNzIjoiZGlkOmtleTp2ZXJpZmllciIsInJlc3BvbnNlX21vZGUiOiJkaXJlY3RfcG9zdCIsInJlc3BvbnNlX3R5cGUiOiJ2cF90b2tlbiIsInJlc3BvbnNlX3VyaSI6Imh0dHBzOi8vdmVyaWZpZXIub3JnL2FwaS92MS9hdXRoZW50aWNhdGlvbl9yZXNwb25zZSIsInNjb3BlIjoib3BlbmlkIiwic3RhdGUiOiJyYW5kb21TdGF0ZSJ9", - }, - {testName: "If all parameters, including the nonce, are set, a proper connection string byValue should be returned.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testSessionId: "my-super-random-id", testClientId: "", testNonce: "my-nonce", requestMode: REQUEST_MODE_BY_VALUE, credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://client.org/callback", - expectedConnection: "openid4vp://?client_id=did:key:verifier&request=eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ.eyJjbGllbnRfaWQiOiJkaWQ6a2V5OnZlcmlmaWVyIiwiZXhwIjozMCwiaXNzIjoiZGlkOmtleTp2ZXJpZmllciIsIm5vbmNlIjoibXktbm9uY2UiLCJyZXNwb25zZV9tb2RlIjoiZGlyZWN0X3Bvc3QiLCJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJyZXNwb25zZV91cmkiOiJodHRwczovL3ZlcmlmaWVyLm9yZy9hcGkvdjEvYXV0aGVudGljYXRpb25fcmVzcG9uc2UiLCJzY29wZSI6Im9wZW5pZCIsInN0YXRlIjoicmFuZG9tU3RhdGUifQ", sessionCacheError: nil, expectedError: nil, - }, - {testName: "If all parameters are set, including the nonce, a proper connection string byReference should be returned.", testHost: "verifier.org", testProtocol: "https", testAddress: "https://client.org/callback", testSessionId: "my-super-random-id", testClientId: "", testNonce: "my-nonce", requestMode: REQUEST_MODE_BY_REFERENCE, credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://client.org/callback", - expectedConnection: "openid4vp://?client_id=did:key:verifier&request_uri=verifier.org/api/v1/request/randomState&request_uri_method=get", sessionCacheError: nil, expectedError: nil, testRequestObjectJwt: "eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ.eyJjbGllbnRfaWQiOiJkaWQ6a2V5OnZlcmlmaWVyIiwiZXhwIjozMCwiaXNzIjoiZGlkOmtleTp2ZXJpZmllciIsIm5vbmNlIjoibXktbm9uY2UiLCJyZXNwb25zZV9tb2RlIjoiZGlyZWN0X3Bvc3QiLCJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJyZXNwb25zZV91cmkiOiJodHRwczovL3ZlcmlmaWVyLm9yZy9hcGkvdjEvYXV0aGVudGljYXRpb25fcmVzcG9uc2UiLCJzY29wZSI6Im9wZW5pZCIsInN0YXRlIjoicmFuZG9tU3RhdGUifQ", - }, - } -} - func TestStartSameDeviceFlow(t *testing.T) { cacheFailError := errors.New("cache_fail") @@ -384,14 +397,18 @@ func TestStartSameDeviceFlow(t *testing.T) { testKey := getECDSAKey() tests := []siopInitTest{ - {testName: "If the request cannot be cached, an error should be responded.", testHost: "verifier.org", testProtocol: "https", testAddress: "/redirect", testSessionId: "my-random-session-id", testClientId: "", credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://verifier.org/redirect", + {testName: "If the request cannot be cached, an error should be responded.", testHost: "verifier.org", testProtocol: "https", testAddress: "/redirect", testState: "my-random-session-id", testClientId: "", credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://verifier.org/redirect", requestMode: REQUEST_MODE_BY_VALUE, expectedConnection: "", sessionCacheError: cacheFailError, expectedError: cacheFailError, }, - {testName: "If everything is provided, a samedevice flow should be started.", testHost: "verifier.org", testProtocol: "https", testAddress: "/redirect", testSessionId: "my-random-session-id", testClientId: "", credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://verifier.org/redirect", - requestMode: REQUEST_MODE_BY_VALUE, expectedConnection: "https://verifier.org/redirect?client_id=did:key:verifier&request=eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ.eyJjbGllbnRfaWQiOiJkaWQ6a2V5OnZlcmlmaWVyIiwiZXhwIjozMCwiaXNzIjoiZGlkOmtleTp2ZXJpZmllciIsInJlc3BvbnNlX21vZGUiOiJkaXJlY3RfcG9zdCIsInJlc3BvbnNlX3R5cGUiOiJ2cF90b2tlbiIsInJlc3BvbnNlX3VyaSI6Imh0dHBzOi8vdmVyaWZpZXIub3JnL2FwaS92MS9hdXRoZW50aWNhdGlvbl9yZXNwb25zZSIsInNjb3BlIjoib3BlbmlkIiwic3RhdGUiOiJyYW5kb21TdGF0ZSJ9", sessionCacheError: nil, expectedError: nil, + {testName: "If everything is provided, a samedevice flow should be started in by_value mode.", testHost: "verifier.org", testProtocol: "https", testAddress: "/redirect", testState: "my-random-session-id", testClientId: "", credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://verifier.org/redirect", + requestMode: REQUEST_MODE_BY_VALUE, expectedConnection: "https://verifier.org/redirect?client_id=did:key:verifier&request=eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ.eyJjbGllbnRfaWQiOiJkaWQ6a2V5OnZlcmlmaWVyIiwiZXhwIjozMCwiaXNzIjoiZGlkOmtleTp2ZXJpZmllciIsIm5vbmNlIjoicmFuZG9tTm9uY2UiLCJyZXNwb25zZV9tb2RlIjoiZGlyZWN0X3Bvc3QiLCJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJyZXNwb25zZV91cmkiOiJodHRwczovL3ZlcmlmaWVyLm9yZy9hcGkvdjEvYXV0aGVudGljYXRpb25fcmVzcG9uc2UiLCJzdGF0ZSI6Im15LXJhbmRvbS1zZXNzaW9uLWlkIn0", sessionCacheError: nil, expectedError: nil, + }, + {testName: "If everything is provided, a samedevice flow should be started in by_reference mode.", testHost: "verifier.org", testProtocol: "https", testAddress: "/redirect", testState: "my-random-session-id", testClientId: "", credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://verifier.org/redirect", + requestMode: REQUEST_MODE_BY_REFERENCE, expectedConnection: "https://verifier.org/redirect?client_id=did:key:verifier&request_uri=verifier.org/api/v1/request/my-random-session-id&request_uri_method=get", sessionCacheError: nil, expectedError: nil, testRequestObjectJwt: "eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ.eyJjbGllbnRfaWQiOiJkaWQ6a2V5OnZlcmlmaWVyIiwiZXhwIjozMCwiaXNzIjoiZGlkOmtleTp2ZXJpZmllciIsIm5vbmNlIjoicmFuZG9tTm9uY2UiLCJyZXNwb25zZV9tb2RlIjoiZGlyZWN0X3Bvc3QiLCJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJyZXNwb25zZV91cmkiOiJodHRwczovL3ZlcmlmaWVyLm9yZy9hcGkvdjEvYXV0aGVudGljYXRpb25fcmVzcG9uc2UiLCJzdGF0ZSI6Im15LXJhbmRvbS1zZXNzaW9uLWlkIn0", }, - {testName: "If everything is provided, a samedevice flow should be started.", testHost: "verifier.org", testProtocol: "https", testAddress: "/redirect", testSessionId: "my-random-session-id", testClientId: "", credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://verifier.org/redirect", - requestMode: REQUEST_MODE_BY_REFERENCE, expectedConnection: "https://verifier.org/redirect?client_id=did:key:verifier&request_uri=verifier.org/api/v1/request/randomState&request_uri_method=get", sessionCacheError: nil, expectedError: nil, testRequestObjectJwt: "eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ.eyJjbGllbnRfaWQiOiJkaWQ6a2V5OnZlcmlmaWVyIiwiZXhwIjozMCwiaXNzIjoiZGlkOmtleTp2ZXJpZmllciIsInJlc3BvbnNlX21vZGUiOiJkaXJlY3RfcG9zdCIsInJlc3BvbnNlX3R5cGUiOiJ2cF90b2tlbiIsInJlc3BvbnNlX3VyaSI6Imh0dHBzOi8vdmVyaWZpZXIub3JnL2FwaS92MS9hdXRoZW50aWNhdGlvbl9yZXNwb25zZSIsInNjb3BlIjoib3BlbmlkIiwic3RhdGUiOiJyYW5kb21TdGF0ZSJ9", + {testName: "If everything is provided, a samedevice flow should be started.", testHost: "verifier.org", testProtocol: "https", testAddress: "/redirect", testState: "my-random-session-id", testClientId: "", credentialScopes: createMockCredentials("", "", "", "", "", false), mockConfigError: nil, expectedCallback: "https://verifier.org/redirect", + requestMode: REQUEST_MODE_BY_REFERENCE, expectedConnection: "https://verifier.org/redirect?client_id=did:key:verifier&request_uri=verifier.org/api/v1/request/my-random-session-id&request_uri_method=get", sessionCacheError: nil, expectedError: nil, testRequestObjectJwt: "eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ.eyJjbGllbnRfaWQiOiJkaWQ6a2V5OnZlcmlmaWVyIiwiZXhwIjozMCwiaXNzIjoiZGlkOmtleTp2ZXJpZmllciIsIm5vbmNlIjoicmFuZG9tTm9uY2UiLCJyZXNwb25zZV9tb2RlIjoiZGlyZWN0X3Bvc3QiLCJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJyZXNwb25zZV91cmkiOiJodHRwczovL3ZlcmlmaWVyLm9yZy9hcGkvdjEvYXV0aGVudGljYXRpb25fcmVzcG9uc2UiLCJzdGF0ZSI6Im15LXJhbmRvbS1zZXNzaW9uLWlkIn0", + testScope: "test", }, } @@ -399,10 +416,10 @@ func TestStartSameDeviceFlow(t *testing.T) { t.Run(tc.testName, func(t *testing.T) { logging.Log().Info("TestSameDeviceFlow +++++++++++++++++ Running test: ", tc.testName) sessionCache := mockSessionCache{sessions: map[string]loginSession{}, errorToThrow: tc.sessionCacheError} - nonceGenerator := mockNonceGenerator{staticValues: []string{"randomState", "randomNonce"}} + nonceGenerator := mockNonceGenerator{staticValues: []string{"randomNonce"}} credentialsConfig := mockCredentialConfig{tc.credentialScopes, tc.mockConfigError} verifier := CredentialVerifier{host: tc.testHost, did: "did:key:verifier", sessionCache: &sessionCache, nonceGenerator: &nonceGenerator, tokenSigner: mockTokenSigner{}, clock: mockClock{}, requestSigningKey: &testKey, credentialsConfig: credentialsConfig, clientIdentification: configModel.ClientIdentification{Id: "did:key:verifier", KeyPath: "/my-signing-key.pem", KeyAlgorithm: "ES256"}} - authReq, err := verifier.StartSameDeviceFlow(tc.testHost, tc.testProtocol, tc.testSessionId, tc.testAddress, tc.testClientId, tc.requestMode) + authReq, err := verifier.StartSameDeviceFlow(tc.testHost, tc.testProtocol, tc.testState, tc.testAddress, tc.testClientId, tc.requestMode, tc.testScope, tc.testRequestProtocol) verifyInitTest(t, tc, authReq, err, sessionCache, SAME_DEVICE) }) } @@ -790,26 +807,6 @@ func (br badRandom) Read(p []byte) (n int, err error) { return len(p), nil } -// compare the payload of two JWTs while ignoring the kid field -func tokenEquals(receivedToken, expectedToken string) bool { - if receivedToken == "" && expectedToken == "" { - return true - } - parsedReceivedToken, err := jwt.ParseString(receivedToken) - if err != nil { - return false - } - parsedReceivedToken.Remove("kid") - - parsedExpectedToken, err := jwt.ParseString(expectedToken) - if err != nil { - return false - } - parsedExpectedToken.Remove("kid") - - return cmp.Equal(parsedReceivedToken, parsedExpectedToken) -} - type openIdProviderMetadataTest struct { host string testName string