Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add API to update user OAuth #397

Merged
merged 3 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading