From d6d2a61c7733db8b1ae3b818afe18e6047ee2d29 Mon Sep 17 00:00:00 2001 From: Bar Saar Date: Wed, 21 Feb 2024 11:21:11 +0200 Subject: [PATCH] add sign in / sign up oauth (#394) * add sign in / sign up oauth depercate start oauth add tests * update naming and desc * update readme --------- Co-authored-by: Gil Shapira --- README.md | 2 +- descope/api/client.go | 20 ++++++++-- descope/internal/auth/auth.go | 12 +++++- descope/internal/auth/oauth.go | 20 +++++++++- descope/internal/auth/oauth_test.go | 38 +++++++++++++++++-- descope/sdk/auth.go | 23 ++++++++++- .../tests/mocks/auth/authenticationmock.go | 33 ++++++++++++++++ 7 files changed, 135 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index f7b2c8b7..26ed09be 100644 --- a/README.md +++ b/README.md @@ -229,7 +229,7 @@ Users can authenticate using their social logins, using the OAuth protocol. Conf // If configured globally, the return URL is optional. If provided however, it will be used // instead of any global configuration. // Redirect the user to the returned URL to start the OAuth redirect chain -url, err := descopeClient.Auth.OAuth().Start(context.Background(), "google", "https://my-app.com/handle-oauth", nil, nil, w) +url, err := descopeClient.Auth.OAuth().SignUpOrIn(context.Background(), "google", "https://my-app.com/handle-oauth", nil, nil, w) if err != nil { // handle error } diff --git a/descope/api/client.go b/descope/api/client.go index 417a8ea4..e6c4369d 100644 --- a/descope/api/client.go +++ b/descope/api/client.go @@ -58,7 +58,9 @@ var ( verifyEnchantedLink: "auth/enchantedlink/verify", getEnchantedLinkSession: "auth/enchantedlink/pending-session", updateUserEmailEnchantedLink: "auth/enchantedlink/update/email", - oauthStart: "auth/oauth/authorize", + oauthSignUpOrIn: "auth/oauth/authorize", + oauthSignUp: "auth/oauth/authorize/signup", + oauthSignIn: "auth/oauth/authorize/signin", exchangeTokenOAuth: "auth/oauth/exchange", samlStart: "auth/saml/authorize", exchangeTokenSAML: "auth/saml/exchange", @@ -232,7 +234,9 @@ type authEndpoints struct { verifyEnchantedLink string getEnchantedLinkSession string updateUserEmailEnchantedLink string - oauthStart string + oauthSignUpOrIn string + oauthSignUp string + oauthSignIn string exchangeTokenOAuth string samlStart string ssoStart string @@ -469,8 +473,16 @@ func (e *endpoints) GetEnchantedLinkSession() string { return path.Join(e.version, e.auth.getEnchantedLinkSession) } -func (e *endpoints) OAuthStart() string { - return path.Join(e.version, e.auth.oauthStart) +func (e *endpoints) OAuthSignUpOrIn() string { + return path.Join(e.version, e.auth.oauthSignUpOrIn) +} + +func (e *endpoints) OAuthSignIn() string { + return path.Join(e.version, e.auth.oauthSignIn) +} + +func (e *endpoints) OAuthSignUp() string { + return path.Join(e.version, e.auth.oauthSignUp) } func (e *endpoints) ExchangeTokenOAuth() string { diff --git a/descope/internal/auth/auth.go b/descope/internal/auth/auth.go index d0154d27..428f3f4b 100644 --- a/descope/internal/auth/auth.go +++ b/descope/internal/auth/auth.go @@ -893,8 +893,16 @@ func composeUpdateUserEmailEnchantedLink() string { return api.Routes.UpdateUserEmailEnchantedlink() } -func composeOAuthURL() string { - return api.Routes.OAuthStart() +func composeOAuthSignUpOrInURL() string { + return api.Routes.OAuthSignUpOrIn() +} + +func composeOAuthSignInURL() string { + return api.Routes.OAuthSignIn() +} + +func composeOAuthSignUpURL() string { + return api.Routes.OAuthSignUp() } func composeOAuthExchangeTokenURL() string { diff --git a/descope/internal/auth/oauth.go b/descope/internal/auth/oauth.go index 022337af..b8d7fb9a 100644 --- a/descope/internal/auth/oauth.go +++ b/descope/internal/auth/oauth.go @@ -18,7 +18,7 @@ type oauthStartResponse struct { URL string `json:"url"` } -func (auth *oauth) Start(ctx context.Context, provider descope.OAuthProvider, redirectURL string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter) (url string, err error) { +func (auth *oauth) start(ctx context.Context, provider descope.OAuthProvider, redirectURL string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter, authorizeURL string) (url string, err error) { m := map[string]string{ "provider": string(provider), } @@ -33,7 +33,7 @@ func (auth *oauth) Start(ctx context.Context, provider descope.OAuthProvider, re } } - httpResponse, err := auth.client.DoPostRequest(ctx, composeOAuthURL(), loginOptions, &api.HTTPRequest{QueryParams: m}, pswd) + httpResponse, err := auth.client.DoPostRequest(ctx, authorizeURL, loginOptions, &api.HTTPRequest{QueryParams: m}, pswd) if err != nil { return } @@ -52,6 +52,22 @@ func (auth *oauth) Start(ctx context.Context, provider descope.OAuthProvider, re return } +func (auth *oauth) SignUpOrIn(ctx context.Context, provider descope.OAuthProvider, redirectURL string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter) (url string, err error) { + return auth.start(ctx, provider, redirectURL, r, loginOptions, w, composeOAuthSignUpOrInURL()) +} + +func (auth *oauth) SignUp(ctx context.Context, provider descope.OAuthProvider, redirectURL string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter) (url string, err error) { + return auth.start(ctx, provider, redirectURL, r, loginOptions, w, composeOAuthSignUpURL()) +} + +func (auth *oauth) SignIn(ctx context.Context, provider descope.OAuthProvider, redirectURL string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter) (url string, err error) { + return auth.start(ctx, provider, redirectURL, r, loginOptions, w, composeOAuthSignInURL()) +} + +func (auth *oauth) Start(ctx context.Context, provider descope.OAuthProvider, redirectURL string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter) (url string, err error) { + return auth.SignUpOrIn(ctx, provider, redirectURL, r, loginOptions, w) +} + func (auth *oauth) ExchangeToken(ctx context.Context, code string, w http.ResponseWriter) (*descope.AuthenticationInfo, error) { return auth.exchangeToken(ctx, code, composeOAuthExchangeTokenURL(), w) } diff --git a/descope/internal/auth/oauth_test.go b/descope/internal/auth/oauth_test.go index 997de79f..3d671327 100644 --- a/descope/internal/auth/oauth_test.go +++ b/descope/internal/auth/oauth_test.go @@ -23,7 +23,7 @@ func TestOAuthStartForwardResponse(t *testing.T) { landingURL := "https://test.com" provider := descope.OAuthGithub a, err := newTestAuth(nil, DoRedirect(uri, func(r *http.Request) { - assert.EqualValues(t, fmt.Sprintf("%s?provider=%s&redirectURL=%s", composeOAuthURL(), provider, url.QueryEscape(landingURL)), r.URL.RequestURI()) + assert.EqualValues(t, fmt.Sprintf("%s?provider=%s&redirectURL=%s", composeOAuthSignUpOrInURL(), provider, url.QueryEscape(landingURL)), r.URL.RequestURI()) })) require.NoError(t, err) w := httptest.NewRecorder() @@ -34,12 +34,44 @@ func TestOAuthStartForwardResponse(t *testing.T) { assert.EqualValues(t, http.StatusTemporaryRedirect, w.Result().StatusCode) } +func TestOAuthSignInForwardResponse(t *testing.T) { + uri := "http://test.me" + landingURL := "https://test.com" + provider := descope.OAuthGithub + a, err := newTestAuth(nil, DoRedirect(uri, func(r *http.Request) { + assert.EqualValues(t, fmt.Sprintf("%s?provider=%s&redirectURL=%s", composeOAuthSignInURL(), provider, url.QueryEscape(landingURL)), r.URL.RequestURI()) + })) + require.NoError(t, err) + w := httptest.NewRecorder() + urlStr, err := a.OAuth().SignIn(context.Background(), provider, landingURL, nil, nil, w) + require.NoError(t, err) + assert.EqualValues(t, uri, urlStr) + assert.EqualValues(t, urlStr, w.Result().Header.Get(descope.RedirectLocationCookieName)) + assert.EqualValues(t, http.StatusTemporaryRedirect, w.Result().StatusCode) +} + +func TestOAuthSignUpForwardResponse(t *testing.T) { + uri := "http://test.me" + landingURL := "https://test.com" + provider := descope.OAuthGithub + a, err := newTestAuth(nil, DoRedirect(uri, func(r *http.Request) { + assert.EqualValues(t, fmt.Sprintf("%s?provider=%s&redirectURL=%s", composeOAuthSignUpURL(), provider, url.QueryEscape(landingURL)), r.URL.RequestURI()) + })) + require.NoError(t, err) + w := httptest.NewRecorder() + urlStr, err := a.OAuth().SignUp(context.Background(), provider, landingURL, nil, nil, w) + require.NoError(t, err) + assert.EqualValues(t, uri, urlStr) + assert.EqualValues(t, urlStr, w.Result().Header.Get(descope.RedirectLocationCookieName)) + assert.EqualValues(t, http.StatusTemporaryRedirect, w.Result().StatusCode) +} + func TestOAuthStartForwardResponseStepup(t *testing.T) { uri := "http://test.me" landingURL := "https://test.com" provider := descope.OAuthGithub a, err := newTestAuth(nil, DoRedirect(uri, func(r *http.Request) { - assert.EqualValues(t, fmt.Sprintf("%s?provider=%s&redirectURL=%s", composeOAuthURL(), provider, url.QueryEscape(landingURL)), r.URL.RequestURI()) + assert.EqualValues(t, fmt.Sprintf("%s?provider=%s&redirectURL=%s", composeOAuthSignUpOrInURL(), provider, url.QueryEscape(landingURL)), r.URL.RequestURI()) body, err := readBodyMap(r) require.NoError(t, err) assert.EqualValues(t, map[string]interface{}{"stepup": true, "customClaims": map[string]interface{}{"k1": "v1"}}, body) @@ -74,7 +106,7 @@ func TestOAuthStartForwardResponseStepupNoJWT(t *testing.T) { func TestOAuthStartInvalidForwardResponse(t *testing.T) { provider := descope.OAuthGithub a, err := newTestAuth(nil, DoBadRequest(func(r *http.Request) { - assert.EqualValues(t, fmt.Sprintf("%s?provider=%s", composeOAuthURL(), provider), r.URL.RequestURI()) + assert.EqualValues(t, fmt.Sprintf("%s?provider=%s", composeOAuthSignUpOrInURL(), provider), r.URL.RequestURI()) })) require.NoError(t, err) w := httptest.NewRecorder() diff --git a/descope/sdk/auth.go b/descope/sdk/auth.go index 5f90b257..4627bcd7 100644 --- a/descope/sdk/auth.go +++ b/descope/sdk/auth.go @@ -178,14 +178,35 @@ type Password interface { } type OAuth interface { - // Start - Use to start an OAuth authentication using the given OAuthProvider. + // Start [Deprecated: Use SignUpOrIn instead] - Use to start an OAuth authentication using the given OAuthProvider. // returns an error upon failure and a string represent the redirect URL upon success. // Uses the response writer to automatically redirect the client to the provider url for authentication. // A successful authentication will result in a callback to the url defined in the current project settings. Start(ctx context.Context, provider descope.OAuthProvider, returnURL string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter) (string, error) + // SignUpOrIn - Use to start an OAuth authentication using the given OAuthProvider. + // returns an error upon failure and a string represent the redirect URL upon success. + // Uses the response writer to automatically redirect the client to the provider url for authentication. + // A successful authentication will result in a callback to the url defined in the current project settings. + SignUpOrIn(ctx context.Context, provider descope.OAuthProvider, returnURL string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter) (string, error) + + // SignUp - Use to start an OAuth authentication using the given OAuthProvider to create a new user. + // returns an error upon failure and a string represent the redirect URL upon success. + // Uses the response writer to automatically redirect the client to the provider url for authentication. + // A successful authentication will result in a callback to the url defined in the current project settings. + SignUp(ctx context.Context, provider descope.OAuthProvider, returnURL string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter) (string, error) + + // SignIn - Use to start an OAuth authentication using the given OAuthProvider to sign in an existing user. + // returns an error upon failure and a string represent the redirect URL upon success. + // Uses the response writer to automatically redirect the client to the provider url for authentication. + // A successful authentication will result in a callback to the url defined in the current project settings. + SignIn(ctx context.Context, provider descope.OAuthProvider, returnURL string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter) (string, error) + // ExchangeToken - Finalize OAuth // code should be extracted from the redirect URL of OAth/SAML authentication flow + // **Important:** The redirect URL might not contain a code URL parameter + // but can contain an `err` URL parameter instead. This can occur when attempting to + // [signUp] with an existing user or trying to [signIn] with a non-existing user. ExchangeToken(ctx context.Context, code string, w http.ResponseWriter) (*descope.AuthenticationInfo, error) } diff --git a/descope/tests/mocks/auth/authenticationmock.go b/descope/tests/mocks/auth/authenticationmock.go index 62679056..b123da4f 100644 --- a/descope/tests/mocks/auth/authenticationmock.go +++ b/descope/tests/mocks/auth/authenticationmock.go @@ -364,6 +364,18 @@ type MockOAuth struct { StartError error StartResponse string + SignUpOrInAssert func(provider descope.OAuthProvider, returnURL string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter) + SignUpOrInError error + SignUpOrInResponse string + + SignInAssert func(provider descope.OAuthProvider, returnURL string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter) + SignInError error + SignInResponse string + + SignUpAssert func(provider descope.OAuthProvider, returnURL string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter) + SignUpError error + SignUpResponse string + ExchangeTokenAssert func(code string, w http.ResponseWriter) ExchangeTokenError error ExchangeTokenResponse *descope.AuthenticationInfo @@ -376,6 +388,27 @@ func (m *MockOAuth) Start(_ context.Context, provider descope.OAuthProvider, ret return m.StartResponse, m.StartError } +func (m *MockOAuth) SignUpOrIn(_ context.Context, provider descope.OAuthProvider, returnURL string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter) (string, error) { + if m.SignUpOrInAssert != nil { + m.SignUpOrInAssert(provider, returnURL, r, loginOptions, w) + } + return m.SignUpOrInResponse, m.SignUpOrInError +} + +func (m *MockOAuth) SignIn(_ context.Context, provider descope.OAuthProvider, returnURL string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter) (string, error) { + if m.SignInAssert != nil { + m.SignInAssert(provider, returnURL, r, loginOptions, w) + } + return m.SignInResponse, m.SignInError +} + +func (m *MockOAuth) SignUp(_ context.Context, provider descope.OAuthProvider, returnURL string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter) (string, error) { + if m.SignUpAssert != nil { + m.SignUpAssert(provider, returnURL, r, loginOptions, w) + } + return m.SignUpResponse, m.SignUpError +} + func (m *MockOAuth) ExchangeToken(_ context.Context, code string, w http.ResponseWriter) (*descope.AuthenticationInfo, error) { if m.ExchangeTokenAssert != nil { m.ExchangeTokenAssert(code, w)