package authentication import ( "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "net/url" "testing" "time" "github.com/auth0/go-auth0/authentication/ciba" "github.com/auth0/go-auth0/internal/client" "github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwt" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/auth0/go-auth0/authentication/oauth" ) func TestLoginWithGrant(t *testing.T) { t.Run("Should return token for CIBA", func(t *testing.T) { // This test required approval on Guardian MFA application. // Hence, it cannot be recorded and is only for manual testing. t.Skip("Skipped as cannot be test in E2E scenario") // Call the Initiate method of the CIBA manager resp, err := authAPI.CIBA.Initiate(context.Background(), ciba.Request{ ClientID: clientID, ClientSecret: clientSecret, Scope: "openid", LoginHint: map[string]string{ "format": "iss_sub", "iss": "https://witty-silver-sailfish-sus1-staging-20240704.sus.auth0.com/", "sub": "auth0|6707939cad3d8bec47ecfa2e", }, BindingMessage: "TEST-BINDING-MESSAGE", }) assert.Empty(t, err) token, err := authAPI.OAuth.LoginWithGrant(context.Background(), "urn:openid:params:grant-type:ciba", url.Values{ "auth_req_id": []string{resp.AuthReqID}, "client_id": []string{clientID}, "client_secret": []string{clientSecret}, }, oauth.IDTokenValidationOptions{}) assert.Empty(t, err) assert.NotEmpty(t, token.AccessToken) }) } func TestOAuthLoginWithPassword(t *testing.T) { auth, err := New( context.Background(), domain, WithClientID(clientID), ) require.NoError(t, err) t.Run("Should return tokens", func(t *testing.T) { configureHTTPTestRecordings(t, auth) user := givenAUser(t) tokenSet, err := auth.OAuth.LoginWithPassword(context.Background(), oauth.LoginWithPasswordRequest{ Username: user.username, Password: user.password, }, oauth.IDTokenValidationOptions{}) require.NoError(t, err) assert.NotEmpty(t, tokenSet.AccessToken) assert.Equal(t, "Bearer", tokenSet.TokenType) }) t.Run("Should support passing extra options", func(t *testing.T) { configureHTTPTestRecordings(t, auth) user := givenAUser(t) tokenSet, err := auth.OAuth.LoginWithPassword(context.Background(), oauth.LoginWithPasswordRequest{ Username: user.username, Password: user.password, Scope: "extra-scope", ExtraParameters: map[string]string{ "extra": "value", }, }, oauth.IDTokenValidationOptions{}) assert.NoError(t, err) assert.NotEmpty(t, tokenSet.AccessToken) assert.Equal(t, "Bearer", tokenSet.TokenType) }) } func TestLoginWithAuthCode(t *testing.T) { t.Run("Should require client_secret or client assertion", func(t *testing.T) { _, err := authAPI.OAuth.LoginWithAuthCode(context.Background(), oauth.LoginWithAuthCodeRequest{ Code: "my-code", }, oauth.IDTokenValidationOptions{}) assert.ErrorContains(t, err, "client_secret or client_assertion is required but not provided") }) t.Run("Should throw for an invalid code", func(t *testing.T) { configureHTTPTestRecordings(t, authAPI) _, err := authAPI.OAuth.LoginWithAuthCode(context.Background(), oauth.LoginWithAuthCodeRequest{ ClientAuthentication: oauth.ClientAuthentication{ ClientSecret: clientSecret, }, Code: "my-invalid-code", }, oauth.IDTokenValidationOptions{}) assert.Error(t, err, "Invalid authorization code") }) t.Run("Should return tokens", func(t *testing.T) { skipE2E(t) configureHTTPTestRecordings(t, authAPI) tokenSet, err := authAPI.OAuth.LoginWithAuthCode(context.Background(), oauth.LoginWithAuthCodeRequest{ ClientAuthentication: oauth.ClientAuthentication{ ClientSecret: clientSecret, }, Code: "my-code", }, oauth.IDTokenValidationOptions{}) assert.NoError(t, err) assert.NotEmpty(t, tokenSet.AccessToken) assert.Equal(t, "Bearer", tokenSet.TokenType) }) t.Run("Should support setting a redirect uri", func(t *testing.T) { skipE2E(t) configureHTTPTestRecordings(t, authAPI) tokenSet, err := authAPI.OAuth.LoginWithAuthCode(context.Background(), oauth.LoginWithAuthCodeRequest{ ClientAuthentication: oauth.ClientAuthentication{ ClientSecret: clientSecret, }, Code: "test-code", RedirectURI: "http://localhost:3000", }, oauth.IDTokenValidationOptions{}) assert.NoError(t, err) assert.NotEmpty(t, tokenSet.AccessToken) assert.Equal(t, "Bearer", tokenSet.TokenType) }) } func TestLoginWithAuthCodeWithPKCE(t *testing.T) { t.Run("Should throw for an invalid code", func(t *testing.T) { configureHTTPTestRecordings(t, authAPI) _, err := authAPI.OAuth.LoginWithAuthCodeWithPKCE(context.Background(), oauth.LoginWithAuthCodeWithPKCERequest{ Code: "test-invalid-code", CodeVerifier: "test-code-verifier", }, oauth.IDTokenValidationOptions{}) assert.Error(t, err, "Invalid authorization code") }) t.Run("Should throw for an invalid code verifier", func(t *testing.T) { configureHTTPTestRecordings(t, authAPI) _, err := authAPI.OAuth.LoginWithAuthCodeWithPKCE(context.Background(), oauth.LoginWithAuthCodeWithPKCERequest{ Code: "test-code", CodeVerifier: "test-invalid-code-verifier", }, oauth.IDTokenValidationOptions{}) assert.Error(t, err, "Failed to verify code verifier") }) t.Run("Should return tokens", func(t *testing.T) { skipE2E(t) configureHTTPTestRecordings(t, authAPI) tokenSet, err := authAPI.OAuth.LoginWithAuthCodeWithPKCE(context.Background(), oauth.LoginWithAuthCodeWithPKCERequest{ Code: "test-code", CodeVerifier: "test-code-verifier", }, oauth.IDTokenValidationOptions{}) assert.NoError(t, err) assert.NotEmpty(t, tokenSet.AccessToken) assert.Equal(t, "Bearer", tokenSet.TokenType) }) t.Run("Should support setting a redirect uri", func(t *testing.T) { skipE2E(t) configureHTTPTestRecordings(t, authAPI) tokenSet, err := authAPI.OAuth.LoginWithAuthCodeWithPKCE(context.Background(), oauth.LoginWithAuthCodeWithPKCERequest{ Code: "test-code", CodeVerifier: "test-code-verifier", RedirectURI: "http://localhost:3000", }, oauth.IDTokenValidationOptions{}) assert.NoError(t, err) assert.NotEmpty(t, tokenSet.AccessToken) assert.Equal(t, "Bearer", tokenSet.TokenType) }) } func TestLoginWithClientCredentials(t *testing.T) { t.Run("Should require client_secret or client assertion", func(t *testing.T) { _, err := authAPI.OAuth.LoginWithClientCredentials(context.Background(), oauth.LoginWithClientCredentialsRequest{ Audience: "test-audience", }, oauth.IDTokenValidationOptions{}) assert.ErrorContains(t, err, "client_secret or client_assertion is required but not provided") }) t.Run("Should return tokens", func(t *testing.T) { skipE2E(t) configureHTTPTestRecordings(t, authAPI) tokenSet, err := authAPI.OAuth.LoginWithClientCredentials(context.Background(), oauth.LoginWithClientCredentialsRequest{ ClientAuthentication: oauth.ClientAuthentication{ ClientSecret: clientSecret, }, Audience: "test-audience", }, oauth.IDTokenValidationOptions{}) assert.NoError(t, err) assert.NotEmpty(t, tokenSet.AccessToken) assert.Equal(t, "Bearer", tokenSet.TokenType) }) t.Run("Should allow overriding clientid", func(t *testing.T) { skipE2E(t) configureHTTPTestRecordings(t, authAPI) tokenSet, err := authAPI.OAuth.LoginWithClientCredentials(context.Background(), oauth.LoginWithClientCredentialsRequest{ ClientAuthentication: oauth.ClientAuthentication{ ClientSecret: clientSecret, ClientID: "test-other-clientid", }, Audience: "test-audience", }, oauth.IDTokenValidationOptions{}) assert.NoError(t, err) assert.NotEmpty(t, tokenSet.AccessToken) assert.Equal(t, "Bearer", tokenSet.TokenType) }) t.Run("Should allow sending extra parameters", func(t *testing.T) { skipE2E(t) configureHTTPTestRecordings(t, authAPI) tokenSet, err := authAPI.OAuth.LoginWithClientCredentials(context.Background(), oauth.LoginWithClientCredentialsRequest{ ClientAuthentication: oauth.ClientAuthentication{ ClientSecret: clientSecret, ClientID: "test-other-clientid", }, Audience: "test-audience", ExtraParameters: map[string]string{ "test": "value", }, }, oauth.IDTokenValidationOptions{}) assert.NoError(t, err) assert.NotEmpty(t, tokenSet.AccessToken) assert.Equal(t, "Bearer", tokenSet.TokenType) }) t.Run("Should support using private key jwt auth", func(t *testing.T) { skipE2E(t) api, err := New( context.Background(), domain, WithIDTokenSigningAlg("HS256"), WithClientID(clientID), WithClientAssertion(jwtPrivateKey, "RS256"), ) require.NoError(t, err) configureHTTPTestRecordings(t, api) tokenSet, err := api.OAuth.LoginWithClientCredentials(context.Background(), oauth.LoginWithClientCredentialsRequest{ Audience: "test-audience", }, oauth.IDTokenValidationOptions{}) assert.NoError(t, err) assert.NotEmpty(t, tokenSet.AccessToken) assert.Equal(t, "Bearer", tokenSet.TokenType) }) t.Run("Should support passing private key jwt auth", func(t *testing.T) { skipE2E(t) configureHTTPTestRecordings(t, authAPI) auth, err := client.CreateClientAssertion("RS256", jwtPrivateKey, clientID, "https://"+domain+"/") require.NoError(t, err) tokenSet, err := authAPI.OAuth.LoginWithClientCredentials(context.Background(), oauth.LoginWithClientCredentialsRequest{ ClientAuthentication: oauth.ClientAuthentication{ ClientAssertion: auth, ClientAssertionType: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", }, Audience: "test-audience", }, oauth.IDTokenValidationOptions{}) assert.NoError(t, err) assert.NotEmpty(t, tokenSet.AccessToken) assert.Equal(t, "Bearer", tokenSet.TokenType) }) t.Run("Should error if provided an invalid algorithm", func(t *testing.T) { api, err := New( context.Background(), domain, WithIDTokenSigningAlg("HS256"), WithClientID(clientID), WithClientAssertion(jwtPrivateKey, "invalid-alg"), ) require.NoError(t, err) _, err = api.OAuth.LoginWithClientCredentials(context.Background(), oauth.LoginWithClientCredentialsRequest{ Audience: "test-audience", }, oauth.IDTokenValidationOptions{}) assert.ErrorContains(t, err, "unsupported client assertion algorithm \"invalid-alg\"") }) t.Run("Should support passing an organization", func(t *testing.T) { skipE2E(t) configureHTTPTestRecordings(t, authAPI) tokenSet, err := authAPI.OAuth.LoginWithClientCredentials(context.Background(), oauth.LoginWithClientCredentialsRequest{ ClientAuthentication: oauth.ClientAuthentication{ ClientSecret: clientSecret, }, Audience: "my-api", Organization: "org_test", }, oauth.IDTokenValidationOptions{}) assert.NoError(t, err) assert.NotEmpty(t, tokenSet.AccessToken) assert.Equal(t, "Bearer", tokenSet.TokenType) }) } func TestRefreshToken(t *testing.T) { t.Run("Should return tokens", func(t *testing.T) { skipE2E(t) configureHTTPTestRecordings(t, authAPI) tokenSet, err := authAPI.OAuth.RefreshToken(context.Background(), oauth.RefreshTokenRequest{ RefreshToken: "test-refresh-token", }, oauth.IDTokenValidationOptions{}) assert.NoError(t, err) assert.NotEmpty(t, tokenSet.AccessToken) assert.NotEmpty(t, tokenSet.RefreshToken) assert.NotEmpty(t, tokenSet.Scope) assert.Equal(t, "Bearer", tokenSet.TokenType) }) t.Run("Should return tokens with reduced scopes", func(t *testing.T) { skipE2E(t) configureHTTPTestRecordings(t, authAPI) tokenSet, err := authAPI.OAuth.RefreshToken(context.Background(), oauth.RefreshTokenRequest{ RefreshToken: "test-refresh-token", Scope: "openid profile offline_access", }, oauth.IDTokenValidationOptions{}) assert.NoError(t, err) assert.NotEmpty(t, tokenSet.AccessToken) assert.NotEmpty(t, tokenSet.RefreshToken) assert.Equal(t, "openid profile offline_access", tokenSet.Scope) assert.Equal(t, "Bearer", tokenSet.TokenType) }) } func TestRevokeRefreshToken(t *testing.T) { t.Run("Should revoke token", func(t *testing.T) { skipE2E(t) configureHTTPTestRecordings(t, authAPI) err := authAPI.OAuth.RevokeRefreshToken(context.Background(), oauth.RevokeRefreshTokenRequest{ Token: "test-refresh-token", }) assert.NoError(t, err) }) t.Run("Should support passing a ClientID and ClientSecret", func(t *testing.T) { skipE2E(t) auth, err := New( context.Background(), domain, WithClientID(clientID), WithClientSecret(clientSecret), WithIDTokenSigningAlg("HS256"), ) assert.NoError(t, err) configureHTTPTestRecordings(t, auth) err = auth.OAuth.RevokeRefreshToken(context.Background(), oauth.RevokeRefreshTokenRequest{ Token: "test-refresh-token", }) assert.NoError(t, err) }) } func TestOAuthWithIDTokenVerification(t *testing.T) { t.Run("error for an invalid organization when using org_id", func(t *testing.T) { extras := map[string]interface{}{ "org_id": "org_123", } api, err := withIDToken(t, extras) assert.NoError(t, err) _, err = api.OAuth.LoginWithAuthCode(context.Background(), oauth.LoginWithAuthCodeRequest{ Code: "my-code", }, oauth.IDTokenValidationOptions{Organization: "org_456"}) assert.ErrorContains(t, err, "org_id claim value mismatch in the ID token") }) t.Run("error for an invalid organization when using org_name", func(t *testing.T) { extras := map[string]interface{}{ "org_name": "wrong-org", } api, err := withIDToken(t, extras) assert.NoError(t, err) _, err = api.OAuth.LoginWithAuthCode(context.Background(), oauth.LoginWithAuthCodeRequest{ Code: "my-code", }, oauth.IDTokenValidationOptions{Organization: "right-org"}) assert.ErrorContains(t, err, "org_name claim value mismatch in the ID token") }) t.Run("error for an invalid nonce", func(t *testing.T) { extras := map[string]interface{}{ "nonce": "wrong-nonce", } api, err := withIDToken(t, extras) assert.NoError(t, err) _, err = api.OAuth.LoginWithAuthCode(context.Background(), oauth.LoginWithAuthCodeRequest{ Code: "my-code", }, oauth.IDTokenValidationOptions{Nonce: "test-nonce"}) assert.ErrorContains(t, err, "nonce claim value mismatch in the ID token; expected") }) t.Run("error for an invalid maxage", func(t *testing.T) { extras := map[string]interface{}{ "auth_time": time.Now().Add(-500 * time.Second).Unix(), } api, err := withIDToken(t, extras) assert.NoError(t, err) _, err = api.OAuth.LoginWithAuthCode(context.Background(), oauth.LoginWithAuthCodeRequest{ Code: "my-code", }, oauth.IDTokenValidationOptions{MaxAge: 100 * time.Second}) assert.ErrorContains(t, err, "auth_time claim in the ID token indicates that too much time has passed") }) } func TestPushedAuthorizationRequest(t *testing.T) { t.Run("Should require a client secret", func(t *testing.T) { _, err := authAPI.OAuth.PushedAuthorization(context.Background(), oauth.PushedAuthorizationRequest{ ResponseType: "code", RedirectURI: "http://localhost:3000/callback", }) assert.ErrorContains(t, err, "client_secret or client_assertion is required but not provided") }) t.Run("Should require a ClientID, ResponseType and RedirectURI", func(t *testing.T) { auth, err := New( context.Background(), domain, ) require.NoError(t, err) _, err = auth.OAuth.PushedAuthorization(context.Background(), oauth.PushedAuthorizationRequest{}) assert.ErrorContains(t, err, "missing required fields: ClientID, ResponseType, RedirectURI") }) t.Run("Should make a PAR request", func(t *testing.T) { skipE2E(t) configureHTTPTestRecordings(t, authAPI) res, err := authAPI.OAuth.PushedAuthorization(context.Background(), oauth.PushedAuthorizationRequest{ ClientAuthentication: oauth.ClientAuthentication{ ClientSecret: clientSecret, }, ResponseType: "code", RedirectURI: "http://localhost:3000/callback", }) require.NoError(t, err) assert.NotEmpty(t, res.RequestURI) assert.NotEmpty(t, res.ExpiresIn) }) t.Run("Should support all arguments", func(t *testing.T) { skipE2E(t) configureHTTPTestRecordings(t, authAPI) res, err := authAPI.OAuth.PushedAuthorization(context.Background(), oauth.PushedAuthorizationRequest{ ClientAuthentication: oauth.ClientAuthentication{ ClientSecret: clientSecret, }, ResponseType: "code", RedirectURI: "http://localhost:3000/callback", Audience: "test-audience", Nonce: "abc123", ResponseMode: "form_post", Scope: "openid profile email", Organization: "my-org", Invitation: "invite", Connection: "Username-Password", CodeChallenge: "n4bQgYhMfWWaL-qgxVrQFaO_TxsrC4Is0V1sFbDwCgg", ExtraParameters: map[string]string{ "test": "value", }, }) require.NoError(t, err) assert.NotEmpty(t, res.RequestURI) assert.NotEmpty(t, res.ExpiresIn) }) } func withIDToken(t *testing.T, extras map[string]interface{}) (*Authentication, error) { t.Helper() idTokenClientSecret := "test-client-secret" idTokenClientid := "test-client-id" var idToken string h := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { tokenSet := &oauth.TokenSet{ AccessToken: "test-access-token", ExpiresIn: 86400, IDToken: idToken, TokenType: "Bearer", } b, err := json.Marshal(tokenSet) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) if _, err := fmt.Fprint(w, string(b)); err != nil { w.WriteHeader(http.StatusInternalServerError) } }) s := httptest.NewTLSServer(h) t.Cleanup(func() { s.Close() }) URL, err := url.Parse(s.URL) assert.NoError(t, err) api, err := New( context.Background(), URL.Host, WithClient(s.Client()), WithClientID(idTokenClientid), WithClientSecret(idTokenClientSecret), WithIDTokenSigningAlg("HS256"), ) assert.NoError(t, err) builder := jwt.NewBuilder(). Issuer(s.URL + "/"). Subject("me"). Audience([]string{idTokenClientid}). Expiration(time.Now().Add(time.Hour)). IssuedAt(time.Now().Add(-time.Hour)) for claim, value := range extras { builder.Claim(claim, value) } token, err := builder.Build() if err != nil { return nil, err } b, err := jwt.Sign(token, jwt.WithKey(jwa.HS256, []byte(idTokenClientSecret))) if err != nil { return nil, err } idToken = string(b) return api, nil }