Skip to content

Commit

Permalink
add sign in / sign up oauth (#394)
Browse files Browse the repository at this point in the history
* add sign in / sign up oauth
depercate start oauth
add tests

* update naming and desc

* update readme

---------

Co-authored-by: Gil Shapira <[email protected]>
  • Loading branch information
Bars92 and shilgapira authored Feb 21, 2024
1 parent c2b8724 commit d6d2a61
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 13 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
20 changes: 16 additions & 4 deletions descope/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
12 changes: 10 additions & 2 deletions descope/internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
20 changes: 18 additions & 2 deletions descope/internal/auth/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
Expand All @@ -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
}
Expand All @@ -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)
}
38 changes: 35 additions & 3 deletions descope/internal/auth/oauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
23 changes: 22 additions & 1 deletion descope/sdk/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
33 changes: 33 additions & 0 deletions descope/tests/mocks/auth/authenticationmock.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down

0 comments on commit d6d2a61

Please sign in to comment.