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

Support new SSO logic #356

Merged
merged 12 commits into from
Dec 31, 2023
75 changes: 68 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,14 +245,28 @@ if err != nil {

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
### SSO SAML/OIDC

Users can authenticate to a specific tenant using SAML or Single Sign On. Configure your SSO/SAML settings on the [Descope console](https://app.descope.com/settings/authentication/sso). To start a flow call:
Users can authenticate to a specific tenant using SAML or OIDC. Configure your SSO SAML/OIDC settings on the [Descope console](https://app.descope.com/settings/authentication/sso). To start a flow call:

```go
// Choose which tenant to log into
// 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 SSO SAML/OIDC redirect chain
url, err := descopeClient.Auth.SSO().Start("my-tenant-ID", "https://my-app.com/handle-saml", nil, nil, w)
if err != nil {
// handle error
}
```


```go
//* Deprecated (use Auth.SSO().Start(..) instead) *//
//
// Choose which tenant to log into
// 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 SSO/SAML redirect chain
url, err := descopeClient.Auth.SAML().Start(context.Background(), "my-tenant-ID", "https://my-app.com/handle-saml", nil, nil, w)
if err != nil {
Expand All @@ -265,6 +279,17 @@ The user will authenticate with the authentication provider configured for that
```go
// The optional `w http.ResponseWriter` adds the session and refresh cookies to the response automatically.
// Otherwise they're available via authInfo
authInfo, err := descopeClient.Auth.SSO().ExchangeToken(context.Background(), code, w)
if err != nil {
// handle error
}
```

```go
//* Deprecated (use Auth.SSO().ExchangeToken(..) instead) *//
//
// The optional `w http.ResponseWriter` adds the session and refresh cookies to the response automatically.
// Otherwise they're available via authInfo
authInfo, err := descopeClient.Auth.SAML().ExchangeToken(context.Background(), code, w)
if err != nil {
// handle error
Expand Down Expand Up @@ -773,24 +798,60 @@ err := descopeClient.Management.AccessKey().Delete(context.Background(), "access

### Manage SSO Setting

You can manage SSO settings and map SSO group roles and user attributes.
You can manage SSO (SAML or OIDC) settings for a specific tenant.

```go
// You can get SSO settings for a specific tenant ID

// Load all tenant SSO settings
ssoSettings, err := cc.HC.DescopeClient().Management.SSO().LoadSettings(context.Background(), "tenant-id")

//* Deprecated (use LoadSettings(..) instead) *//
guyp-descope marked this conversation as resolved.
Show resolved Hide resolved
ssoSettings, err := descopeClient.Management.SSO().GetSettings(context.Background(), "tenant-id")

// You can configure SSO settings manually by setting the required fields directly
// Configure tenant SSO by OIDC settings

oidcSettings := &descope.SSOOIDCSettings{..}
err = cc.HC.DescopeClient().Management.SSO().ConfigureOIDCSettings("tenant-id", oidcSettings, "https://redirectlocation.com", "")
// OR
// Load all tenant SSO settings and use them for configure OIDC settings
ssoSettings, err := cc.HC.DescopeClient().Management.SSO().LoadSettings("tenant-id")
ssoSettings.Oidc.Name = "my prOvider"
ssoSettings.Oidc.AuthURL = authorizeEndpoint
...
ssoSettings.Oidc.Scope = []string{"openid", "profile", "email"}
err = cc.HC.DescopeClient().Management.SSO().ConfigureOIDCSettings("tenant-id", ssoSettings.Oidc, "https://redirectlocation.com", "")

// Configure tenant SSO by SAML settings
tenantID := "tenant-id" // Which tenant this configuration is for
idpURL := "https://idp.com"
entityID := "my-idp-entity-id"
idpCert := "<your-cert-here>"
redirectURL := "https://my-app.com/handle-saml" // Global redirect URL for SSO/SAML
domain := "domain.com" // Users logging in from this domain will be logged in to this tenant
samlSettings := &descope.SSOSAMLSettings{
IdpURL: idpURL,
IdpEntityID: entityID,
IdpCert: idpCert,
AttributeMapping: &descope.AttributeMapping{Email: "myEmail", ..},
RoleMappings: []*RoleMapping{{..}},
}
err = cc.HC.DescopeClient().Management.SSO().ConfigureSAMLSettings(context.Background(), tenantID, samlSettings, redirectURL, domain)

//* Deprecated (use ConfigureSAMLSettings(..) instead) *//
err := descopeClient.Management.SSO().ConfigureSettings(context.Background(), tenantID, idpURL, entityID, idpCert, redirectURL, domain)

// Alternatively, configure using an SSO metadata URL
err := descopeClient.Management.SSO().ConfigureMetadata(context.Background(), tenantID, "https://idp.com/my-idp-metadata", redirectURL, domain)
// Alternatively, configure using an SSO SAML metadata URL
samlSettings := &descope.SSOSAMLSettingsByMetadata{
IdpMetadataURL: "https://idp.com/my-idp-metadata",
AttributeMapping: &descope.AttributeMapping{Email: "myEmail", ..},
RoleMappings: []*RoleMapping{{..}},
}
err = cc.HC.DescopeClient().Management.SSO().ConfigureSAMLSettingsByMetadata(context.Background(), tenantID, samlSettings, redirectURL, domain)

//* Deprecated (use ConfigureSAMLSettingsByMetadata(..) instead) *//
err := descopeClient.Management.SSO().ConfigureMetadata(tenantID, "https://idp.com/my-idp-metadata", redirectURL, domain)

//* Deprecated (use Management.SSO().ConfigureSAMLSettings(..) or Management.SSO().ConfigureSAMLSettingsByMetadata(..) instead) *//
// Map IDP groups to Descope roles, or map user attributes.
// This function overrides any previous mapping (even when empty). Use carefully.
roleMapping := []*descope.RoleMapping{
Expand Down
43 changes: 42 additions & 1 deletion descope/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ var (
exchangeTokenOAuth: "auth/oauth/exchange",
samlStart: "auth/saml/authorize",
exchangeTokenSAML: "auth/saml/exchange",
ssoStart: "auth/sso/authorize",
exchangeTokenSSO: "auth/sso/exchange",
webauthnSignUpStart: "auth/webauthn/signup/start",
webauthnSignUpFinish: "auth/webauthn/signup/finish",
webauthnSignInStart: "auth/webauthn/signin/start",
Expand Down Expand Up @@ -117,6 +119,10 @@ var (
accessKeyActivate: "mgmt/accesskey/activate",
accessKeyDelete: "mgmt/accesskey/delete",
ssoSettings: "mgmt/sso/settings",
ssoLoadSettings: "mgmt/sso/settings", // v2 only
ssoSAMLSettings: "mgmt/sso/samlsettings",
ssoSAMLSettingsByMetadata: "mgmt/sso/samlsettingsbymetadata",
ssoOIDCSettings: "mgmt/sso/oidcsettings",
ssoMetadata: "mgmt/sso/metadata",
ssoMapping: "mgmt/sso/mapping",
updateJWT: "mgmt/jwt/update",
Expand Down Expand Up @@ -207,7 +213,9 @@ type authEndpoints struct {
oauthStart string
exchangeTokenOAuth string
samlStart string
ssoStart string
exchangeTokenSAML string
exchangeTokenSSO string
webauthnSignUpStart string
webauthnSignUpFinish string
webauthnSignInStart string
Expand Down Expand Up @@ -269,10 +277,17 @@ type mgmtEndpoints struct {
accessKeyActivate string
accessKeyDelete string

//* Deprecated (use the below value instead) *//
ssoSettings string
ssoMetadata string
ssoMapping string
updateJWT string
///////////////////

ssoLoadSettings string
ssoSAMLSettings string
ssoSAMLSettingsByMetadata string
ssoOIDCSettings string
updateJWT string

permissionCreate string
permissionUpdate string
Expand Down Expand Up @@ -395,12 +410,23 @@ func (e *endpoints) OAuthStart() string {
func (e *endpoints) ExchangeTokenOAuth() string {
return path.Join(e.version, e.auth.exchangeTokenOAuth)
}

/* Deprecated (use SSOStart(..) instead) */
func (e *endpoints) SAMLStart() string {
return path.Join(e.version, e.auth.samlStart)
}

/* Deprecated (use ExchangeTokenSSO(..) instead) */
func (e *endpoints) ExchangeTokenSAML() string {
return path.Join(e.version, e.auth.exchangeTokenSAML)
}

func (e *endpoints) SSOStart() string {
return path.Join(e.version, e.auth.ssoStart)
}
func (e *endpoints) ExchangeTokenSSO() string {
return path.Join(e.version, e.auth.exchangeTokenSSO)
}
func (e *endpoints) WebAuthnSignUpStart() string {
return path.Join(e.version, e.auth.webauthnSignUpStart)
}
Expand Down Expand Up @@ -625,6 +651,21 @@ func (e *endpoints) ManagementAccessKeyDelete() string {
return path.Join(e.version, e.mgmt.accessKeyDelete)
}

func (e *endpoints) ManagementSSOLoadSettings() string {
return path.Join(e.versionV2, e.mgmt.ssoLoadSettings)
}

func (e *endpoints) ManagementSSOSAMLSettings() string {
return path.Join(e.version, e.mgmt.ssoSAMLSettings)
}
func (e *endpoints) ManagementSSOSAMLSettingsByMetadata() string {
return path.Join(e.version, e.mgmt.ssoSAMLSettingsByMetadata)
}
func (e *endpoints) ManagementSSOOIDCSettings() string {
return path.Join(e.version, e.mgmt.ssoOIDCSettings)
}

// // Deprecated
func (e *endpoints) ManagementSSOSettings() string {
return path.Join(e.version, e.mgmt.ssoSettings)
}
Expand Down
13 changes: 13 additions & 0 deletions descope/internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type authenticationService struct {
webAuthn sdk.WebAuthn
oauth sdk.OAuth
saml sdk.SAML
sso sdk.SSOServiceProvider
}

func NewAuth(conf AuthParams, c *api.Client) (*authenticationService, error) {
Expand All @@ -55,6 +56,7 @@ func NewAuth(conf AuthParams, c *api.Client) (*authenticationService, error) {
authenticationService.enchantedLink = &enchantedLink{authenticationsBase: base}
authenticationService.oauth = &oauth{authenticationsBase: base}
authenticationService.saml = &saml{authenticationsBase: base}
authenticationService.sso = &sso{authenticationsBase: base}
authenticationService.webAuthn = &webAuthn{authenticationsBase: base}
authenticationService.totp = &totp{authenticationsBase: base}
authenticationService.password = &password{authenticationsBase: base}
Expand Down Expand Up @@ -89,6 +91,10 @@ func (auth *authenticationService) SAML() sdk.SAML {
return auth.saml
}

func (auth *authenticationService) SSO() sdk.SSOServiceProvider {
return auth.sso
}

func (auth *authenticationService) WebAuthn() sdk.WebAuthn {
return auth.webAuthn
}
Expand Down Expand Up @@ -869,6 +875,13 @@ func composeSAMLExchangeTokenURL() string {
return api.Routes.ExchangeTokenSAML()
}

func composeSSOStartURL() string {
return api.Routes.SSOStart()
}
func composeSSOExchangeTokenURL() string {
return api.Routes.ExchangeTokenSSO()
}

func composeUpdateUserEmailOTP() string {
return api.Routes.UpdateUserEmailOTP()
}
Expand Down
62 changes: 62 additions & 0 deletions descope/internal/auth/sso.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package auth

import (
"context"
"net/http"

"github.com/descope/go-sdk/descope"
"github.com/descope/go-sdk/descope/api"
"github.com/descope/go-sdk/descope/internal/utils"
"github.com/descope/go-sdk/descope/logger"
)

type sso struct {
authenticationsBase
}

type ssoStartResponse struct {
URL string `json:"url"`
}

func (auth *sso) Start(ctx context.Context, tenant string, redirectURL string, prompt string, r *http.Request, loginOptions *descope.LoginOptions, w http.ResponseWriter) (url string, err error) {
if tenant == "" {
return "", utils.NewInvalidArgumentError("tenant")
}
m := map[string]string{
"tenant": string(tenant),
}
if len(redirectURL) > 0 {
m["redirectURL"] = redirectURL
}
if len(prompt) > 0 {
m["prompt"] = prompt
}
var pswd string
if loginOptions.IsJWTRequired() {
pswd, err = getValidRefreshToken(r)
if err != nil {
return "", descope.ErrInvalidStepUpJWT
}
}
httpResponse, err := auth.client.DoPostRequest(ctx, composeSSOStartURL(), loginOptions, &api.HTTPRequest{QueryParams: m}, pswd)
if err != nil {
return
}

if httpResponse.Res != nil {
res := &ssoStartResponse{}
err = utils.Unmarshal([]byte(httpResponse.BodyStr), res)
if err != nil {
logger.LogError("Failed to parse sso location from response for [%s]", err, tenant)
return "", err
}
url = res.URL
redirectToURL(url, w)
}

return
}

func (auth *sso) ExchangeToken(ctx context.Context, code string, w http.ResponseWriter) (*descope.AuthenticationInfo, error) {
return auth.exchangeToken(ctx, code, composeSSOExchangeTokenURL(), w)
}
Loading