Skip to content

Commit

Permalink
Add API to update user OAuth (#397)
Browse files Browse the repository at this point in the history
  • Loading branch information
talaharoni authored Feb 23, 2024
1 parent d6d2a61 commit bbe393d
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 30 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,16 @@ if err != nil {
}
```

Users can also connect the social login account to their existing user:
```go
// A valid Refresh Token of the existing user is required and will be taken from the request header or cookies automatically.
// If allowAllMerge is 'true' the users will be merged also if there is no common identifier between the social provider and the existing user (like email).
url, err := descopeClient.Auth.OAuth().UpdateUser(context.Background(), "google", "https://my-app.com/handle-oauth", true, nil, nil, w)
if err != nil {
// handle error
}
```

The session and refresh JWTs should be returned to the caller, and passed with every request in the session. Read more on [session validation](#session-validation)

### SSO (SAML / OIDC)
Expand Down
6 changes: 6 additions & 0 deletions descope/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ var (
oauthSignUpOrIn: "auth/oauth/authorize",
oauthSignUp: "auth/oauth/authorize/signup",
oauthSignIn: "auth/oauth/authorize/signin",
oauthUpdateUser: "auth/oauth/authorize/update",
exchangeTokenOAuth: "auth/oauth/exchange",
samlStart: "auth/saml/authorize",
exchangeTokenSAML: "auth/saml/exchange",
Expand Down Expand Up @@ -237,6 +238,7 @@ type authEndpoints struct {
oauthSignUpOrIn string
oauthSignUp string
oauthSignIn string
oauthUpdateUser string
exchangeTokenOAuth string
samlStart string
ssoStart string
Expand Down Expand Up @@ -485,6 +487,10 @@ func (e *endpoints) OAuthSignUp() string {
return path.Join(e.version, e.auth.oauthSignUp)
}

func (e *endpoints) OAuthUpdateUser() string {
return path.Join(e.version, e.auth.oauthUpdateUser)
}

func (e *endpoints) ExchangeTokenOAuth() string {
return path.Join(e.version, e.auth.exchangeTokenOAuth)
}
Expand Down
4 changes: 4 additions & 0 deletions descope/internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -905,6 +905,10 @@ func composeOAuthSignUpURL() string {
return api.Routes.OAuthSignUp()
}

func composeOAuthUpdateUserURL() string {
return api.Routes.OAuthUpdateUser()
}

func composeOAuthExchangeTokenURL() string {
return api.Routes.ExchangeTokenOAuth()
}
Expand Down
51 changes: 39 additions & 12 deletions descope/internal/auth/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package auth
import (
"context"
"net/http"
"strconv"

"github.com/descope/go-sdk/descope"
"github.com/descope/go-sdk/descope/api"
Expand All @@ -18,13 +19,8 @@ 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, authorizeURL string) (url string, err error) {
m := map[string]string{
"provider": string(provider),
}
if len(redirectURL) > 0 {
m["redirectURL"] = redirectURL
}
func (auth *oauth) startAuth(ctx context.Context, provider descope.OAuthProvider, redirectURL string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter, authorizeURL string) (url string, err error) {
m := generateOAuthRequestParams(ctx, provider, redirectURL)
var pswd string
if loginOptions.IsJWTRequired() {
pswd, err = getValidRefreshToken(r)
Expand All @@ -33,7 +29,24 @@ func (auth *oauth) start(ctx context.Context, provider descope.OAuthProvider, re
}
}

httpResponse, err := auth.client.DoPostRequest(ctx, authorizeURL, loginOptions, &api.HTTPRequest{QueryParams: m}, pswd)
return auth.doStart(ctx, provider, m, pswd, loginOptions, w, authorizeURL)
}

func (auth *oauth) startUpdate(ctx context.Context, provider descope.OAuthProvider, redirectURL string, allowAllMerge bool, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter, authorizeURL string) (url string, err error) {
m := generateOAuthRequestParams(ctx, provider, redirectURL)
if allowAllMerge {
m["allowAllMerge"] = strconv.FormatBool(allowAllMerge)
}
pswd, err := getValidRefreshToken(r)
if err != nil {
return "", descope.ErrRefreshToken
}

return auth.doStart(ctx, provider, m, pswd, loginOptions, w, authorizeURL)
}

func (auth *oauth) doStart(ctx context.Context, provider descope.OAuthProvider, params map[string]string, pswd string, loginOptions *descope.LoginOptions, w http.ResponseWriter, authorizeURL string) (url string, err error) {
httpResponse, err := auth.client.DoPostRequest(ctx, authorizeURL, loginOptions, &api.HTTPRequest{QueryParams: params}, pswd)
if err != nil {
return
}
Expand All @@ -48,20 +61,23 @@ func (auth *oauth) start(ctx context.Context, provider descope.OAuthProvider, re
url = res.URL
redirectToURL(url, w)
}

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())
return auth.startAuth(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())
return auth.startAuth(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())
return auth.startAuth(ctx, provider, redirectURL, r, loginOptions, w, composeOAuthSignInURL())
}

func (auth *oauth) UpdateUser(ctx context.Context, provider descope.OAuthProvider, redirectURL string, allowAllMerge bool, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter) (url string, err error) {
return auth.startUpdate(ctx, provider, redirectURL, allowAllMerge, r, loginOptions, w, composeOAuthUpdateUserURL())
}

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) {
Expand All @@ -71,3 +87,14 @@ func (auth *oauth) Start(ctx context.Context, provider descope.OAuthProvider, re
func (auth *oauth) ExchangeToken(ctx context.Context, code string, w http.ResponseWriter) (*descope.AuthenticationInfo, error) {
return auth.exchangeToken(ctx, code, composeOAuthExchangeTokenURL(), w)
}

func generateOAuthRequestParams(_ context.Context, provider descope.OAuthProvider, redirectURL string) map[string]string {
params := map[string]string{
"provider": string(provider),
}
if len(redirectURL) > 0 {
params["redirectURL"] = redirectURL
}

return params
}
27 changes: 27 additions & 0 deletions descope/internal/auth/oauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,33 @@ func TestOAuthSignUpForwardResponse(t *testing.T) {
assert.EqualValues(t, http.StatusTemporaryRedirect, w.Result().StatusCode)
}

func TestOAuthUpdateUserForwardResponse(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?allowAllMerge=%s&provider=%s&redirectURL=%s", composeOAuthUpdateUserURL(), "true", provider, url.QueryEscape(landingURL)), r.URL.RequestURI())
}))
require.NoError(t, err)
w := httptest.NewRecorder()
urlStr, err := a.OAuth().UpdateUser(context.Background(), provider, landingURL, true, &http.Request{Header: http.Header{"Cookie": []string{"DSR=test"}}}, 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 TestOAuthUpdateUserForwardResponsepNoJWT(t *testing.T) {
uri := "http://test.me"
landingURL := "https://test.com"
provider := descope.OAuthGithub
a, err := newTestAuth(nil, DoRedirect(uri, func(r *http.Request) {}))
require.NoError(t, err)
w := httptest.NewRecorder()
_, err = a.OAuth().UpdateUser(context.Background(), provider, landingURL, true, nil, nil, w)
assert.ErrorIs(t, err, descope.ErrRefreshToken)
}

func TestOAuthStartForwardResponseStepup(t *testing.T) {
uri := "http://test.me"
landingURL := "https://test.com"
Expand Down
7 changes: 7 additions & 0 deletions descope/sdk/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,13 @@ type OAuth interface {
// 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)

// UserUpdate - Use to start an OAuth authentication using the given OAuthProvider to enable an existing user to sign in also with OAuth method.
// 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.
// allowAllMerge - allow updating the existing user also if there is no common identifier between the OAuth and the existing user (like email)
UpdateUser(ctx context.Context, provider descope.OAuthProvider, returnURL string, allowAllMerge bool, 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
Expand Down
47 changes: 29 additions & 18 deletions descope/tests/mocks/auth/authenticationmock.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,55 +360,66 @@ func (m *MockPassword) GetPasswordPolicy(_ context.Context) (*descope.PasswordPo
// Mock OAuth

type MockOAuth struct {
StartAssert func(provider descope.OAuthProvider, returnURL string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter)
StartAssert func(provider descope.OAuthProvider, redirectURL string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter)
StartError error
StartResponse string

SignUpOrInAssert func(provider descope.OAuthProvider, returnURL string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter)
SignUpOrInAssert func(provider descope.OAuthProvider, redirectURL 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)
SignInAssert func(provider descope.OAuthProvider, redirectURL 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)
SignUpAssert func(provider descope.OAuthProvider, redirectURL string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter)
SignUpError error
SignUpResponse string

UpdateUserAssert func(provider descope.OAuthProvider, redirectURL string, allowAllMerge bool, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter)
UpdateUserError error
UpdateUserResponse string

ExchangeTokenAssert func(code string, w http.ResponseWriter)
ExchangeTokenError error
ExchangeTokenResponse *descope.AuthenticationInfo
}

func (m *MockOAuth) Start(_ context.Context, provider descope.OAuthProvider, returnURL string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter) (string, error) {
func (m *MockOAuth) Start(_ context.Context, provider descope.OAuthProvider, redirectURL string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter) (string, error) {
if m.StartAssert != nil {
m.StartAssert(provider, returnURL, r, loginOptions, w)
m.StartAssert(provider, redirectURL, r, loginOptions, w)
}
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) {
func (m *MockOAuth) SignUpOrIn(_ context.Context, provider descope.OAuthProvider, redirectURL string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter) (string, error) {
if m.SignUpOrInAssert != nil {
m.SignUpOrInAssert(provider, returnURL, r, loginOptions, w)
m.SignUpOrInAssert(provider, redirectURL, 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) {
func (m *MockOAuth) SignIn(_ context.Context, provider descope.OAuthProvider, redirectURL string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter) (string, error) {
if m.SignInAssert != nil {
m.SignInAssert(provider, returnURL, r, loginOptions, w)
m.SignInAssert(provider, redirectURL, 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) {
func (m *MockOAuth) SignUp(_ context.Context, provider descope.OAuthProvider, redirectURL string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter) (string, error) {
if m.SignUpAssert != nil {
m.SignUpAssert(provider, returnURL, r, loginOptions, w)
m.SignUpAssert(provider, redirectURL, r, loginOptions, w)
}
return m.SignUpResponse, m.SignUpError
}

func (m *MockOAuth) UpdateUser(_ context.Context, provider descope.OAuthProvider, redirectURL string, allowAllMerge bool, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter) (string, error) {
if m.UpdateUserAssert != nil {
m.UpdateUserAssert(provider, redirectURL, allowAllMerge, r, loginOptions, w)
}
return m.UpdateUserResponse, m.UpdateUserError
}

func (m *MockOAuth) ExchangeToken(_ context.Context, code string, w http.ResponseWriter) (*descope.AuthenticationInfo, error) {
if m.ExchangeTokenAssert != nil {
m.ExchangeTokenAssert(code, w)
Expand All @@ -419,7 +430,7 @@ func (m *MockOAuth) ExchangeToken(_ context.Context, code string, w http.Respons
// Mock SAML

type MockSAML struct {
StartAssert func(tenant string, returnURL string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter)
StartAssert func(tenant string, redirectURL string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter)
StartError error
StartResponse string

Expand All @@ -428,9 +439,9 @@ type MockSAML struct {
ExchangeTokenResponse *descope.AuthenticationInfo
}

func (m *MockSAML) Start(_ context.Context, tenant string, returnURL string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter) (redirectURL string, err error) {
func (m *MockSAML) Start(_ context.Context, tenant string, redirectURL string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter) (string, error) {
if m.StartAssert != nil {
m.StartAssert(tenant, returnURL, r, loginOptions, w)
m.StartAssert(tenant, redirectURL, r, loginOptions, w)
}
return m.StartResponse, m.StartError
}
Expand All @@ -445,7 +456,7 @@ func (m *MockSAML) ExchangeToken(_ context.Context, code string, w http.Response
// Mock SSO

type MockSSO struct {
StartAssert func(tenant string, returnURL string, prompt string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter)
StartAssert func(tenant string, redirectURL string, prompt string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter)
StartError error
StartResponse string

Expand All @@ -454,9 +465,9 @@ type MockSSO struct {
ExchangeTokenResponse *descope.AuthenticationInfo
}

func (m *MockSSO) Start(_ context.Context, tenant string, returnURL string, prompt string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter) (redirectURL string, err error) {
func (m *MockSSO) Start(_ context.Context, tenant string, redirectURL string, prompt string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter) (string, error) {
if m.StartAssert != nil {
m.StartAssert(tenant, returnURL, prompt, r, loginOptions, w)
m.StartAssert(tenant, redirectURL, prompt, r, loginOptions, w)
}
return m.StartResponse, m.StartError
}
Expand Down

0 comments on commit bbe393d

Please sign in to comment.