diff --git a/client/logto_config.go b/client/logto_config.go index 72c97aa..34edf33 100644 --- a/client/logto_config.go +++ b/client/logto_config.go @@ -6,12 +6,13 @@ import ( ) type LogtoConfig struct { - Endpoint string - AppId string - AppSecret string - Scopes []string - Resources []string - Prompt string + Endpoint string + AppId string + AppSecret string + Scopes []string + Resources []string + Prompt string + IncludeReservedScopes *bool } /** @@ -21,8 +22,11 @@ type LogtoConfig struct { * - Add `ReservedResource.Organization` to resources if `UserScope.Organizations` is included in scopes. */ func (logtoConfig *LogtoConfig) normalized() { - for _, defaultScope := range core.DefaultScopes { - logtoConfig.Scopes = core.AppendIfNotExisted(logtoConfig.Scopes, defaultScope) + includeReservedScopes := logtoConfig.IncludeReservedScopes + if includeReservedScopes == nil || *includeReservedScopes { + for _, defaultScope := range core.DefaultScopes { + logtoConfig.Scopes = core.AppendIfNotExisted(logtoConfig.Scopes, defaultScope) + } } if slices.Contains(logtoConfig.Scopes, core.UserScopeOrganizations) { diff --git a/client/sign_in.go b/client/sign_in.go index d0a6ff0..350555a 100644 --- a/client/sign_in.go +++ b/client/sign_in.go @@ -6,6 +6,16 @@ import ( "github.com/logto-io/go/core" ) +type SignInOptions struct { + RedirectUri string + Prompt string + FirstScreen string + Identifiers []string + DirectSignIn *core.DirectSignInOptions + LoginHint string + ExtraParams map[string]string +} + type SignInSession struct { RedirectUri string CodeVerifier string @@ -13,7 +23,7 @@ type SignInSession struct { State string } -func (logtoClient *LogtoClient) SignIn(redirectUri string) (string, error) { +func (logtoClient *LogtoClient) SignIn(options *SignInOptions) (string, error) { oidcConfig, fetchOidcConfigErr := logtoClient.fetchOidcConfig() if fetchOidcConfigErr != nil { @@ -24,15 +34,26 @@ func (logtoClient *LogtoClient) SignIn(redirectUri string) (string, error) { codeChallenge := core.GenerateCodeChallenge(codeVerifier) state := core.GenerateState() + prompt := options.Prompt + if prompt == "" { + prompt = logtoClient.logtoConfig.Prompt + } + signInUri, generateSignInUriErr := core.GenerateSignInUri(&core.SignInUriGenerationOptions{ AuthorizationEndpoint: oidcConfig.AuthorizationEndpoint, ClientId: logtoClient.logtoConfig.AppId, - RedirectUri: redirectUri, + RedirectUri: options.RedirectUri, CodeChallenge: codeChallenge, State: state, Scopes: logtoClient.logtoConfig.Scopes, Resources: logtoClient.logtoConfig.Resources, - Prompt: logtoClient.logtoConfig.Prompt, + Prompt: prompt, + FirstScreen: options.FirstScreen, + Identifiers: options.Identifiers, + DirectSignIn: options.DirectSignIn, + LoginHint: options.LoginHint, + ExtraParams: options.ExtraParams, + IncludeReservedScopes: logtoClient.logtoConfig.IncludeReservedScopes, }) if generateSignInUriErr != nil { @@ -40,7 +61,7 @@ func (logtoClient *LogtoClient) SignIn(redirectUri string) (string, error) { } signInSession := SignInSession{ - RedirectUri: redirectUri, + RedirectUri: options.RedirectUri, CodeVerifier: codeVerifier, CodeChallenge: codeChallenge, State: state, @@ -55,3 +76,9 @@ func (logtoClient *LogtoClient) SignIn(redirectUri string) (string, error) { return signInUri, nil } + +func (logtoClient *LogtoClient) SignInWithRedirectUri(redirectUri string) (string, error) { + return logtoClient.SignIn(&SignInOptions{ + RedirectUri: redirectUri, + }) +} diff --git a/client/sign_in_test.go b/client/sign_in_test.go index f2cf8d1..c829e9f 100644 --- a/client/sign_in_test.go +++ b/client/sign_in_test.go @@ -61,7 +61,7 @@ func TestSignInShouldReturnSignInUriCorrectly(t *testing.T) { logtoClient := NewLogtoClient(logtoConfig, storage) - signInUri, signInErr := logtoClient.SignIn(testRedirectUri) + signInUri, signInErr := logtoClient.SignInWithRedirectUri(testRedirectUri) assert.Nil(t, signInErr) assert.Equal(t, testSignInUri, signInUri) diff --git a/core/constants.go b/core/constants.go index 2eb667c..24f3ba7 100644 --- a/core/constants.go +++ b/core/constants.go @@ -1,15 +1,15 @@ package core -var ( +const ( ReservedScopeOpenId = "openid" ReservedScopeOfflineAccess = "offline_access" ) -var ( +const ( ReservedResourceOrganization = "urn:logto:resource:organizations" ) -var ( +const ( UserScopeProfile = "profile" UserScopeEmail = "email" UserScopePhone = "phone" @@ -27,3 +27,48 @@ var ( UserScopeProfile, } ) + +const ( + QueryKeyClientID = "client_id" + QueryKeyRedirectURI = "redirect_uri" + QueryKeyCodeChallenge = "code_challenge" + QueryKeyCodeChallengeMethod = "code_challenge_method" + QueryKeyState = "state" + QueryKeyScope = "scope" + QueryKeyResource = "resource" + QueryKeyResponseType = "response_type" + QueryKeyPrompt = "prompt" + QueryKeyLoginHint = "login_hint" + QueryKeyFirstScreen = "first_screen" + QueryKeyIdentifiers = "identifiers" + QueryKeyDirectSignIn = "direct_sign_in" +) + +const ( + IdentifierEmail = "email" + IdentifierPhone = "phone" + IdentifierUsername = "username" +) + +const ( + DirectSignInMethodSocial = "social" + DirectSignInMethodSso = "sso" +) + +const ( + PromptConsent = "consent" + PromptLogin = "login" +) + +const ( + ResponseTypeCode = "code" +) + +const ( + FirstScreenSignIn = "sign_in" + FirstScreenRegister = "register" + FirstScreenResetPassword = "reset_password" + FirstScreenSingleSignOn = "single_sign_on" + FirstScreenIdentifierSignIn = "identifier:sign_in" + FirstScreenIdentifierRegister = "identifier:register" +) diff --git a/core/sign_in.go b/core/sign_in.go index 8fe4899..fc9aeaf 100644 --- a/core/sign_in.go +++ b/core/sign_in.go @@ -6,6 +6,11 @@ import ( "strings" ) +type DirectSignInOptions struct { + Method string + Target string +} + type SignInUriGenerationOptions struct { AuthorizationEndpoint string ClientId string @@ -15,6 +20,12 @@ type SignInUriGenerationOptions struct { Scopes []string Resources []string Prompt string + LoginHint string + FirstScreen string + Identifiers []string + DirectSignIn *DirectSignInOptions + ExtraParams map[string]string + IncludeReservedScopes *bool } func GenerateSignInUri(option *SignInUriGenerationOptions) (string, error) { @@ -26,18 +37,21 @@ func GenerateSignInUri(option *SignInUriGenerationOptions) (string, error) { queries := uri.Query() - queries.Add("client_id", option.ClientId) - queries.Add("redirect_uri", option.RedirectUri) - queries.Add("code_challenge", option.CodeChallenge) - queries.Add("code_challenge_method", "S256") - queries.Add("state", option.State) + queries.Add(QueryKeyClientID, option.ClientId) + queries.Add(QueryKeyRedirectURI, option.RedirectUri) + queries.Add(QueryKeyCodeChallenge, option.CodeChallenge) + queries.Add(QueryKeyCodeChallengeMethod, "S256") + queries.Add(QueryKeyState, option.State) scopes := option.Scopes - for _, defaultScope := range DefaultScopes { - scopes = AppendIfNotExisted(scopes, defaultScope) + if option.IncludeReservedScopes == nil || *option.IncludeReservedScopes { + for _, defaultScope := range DefaultScopes { + scopes = AppendIfNotExisted(scopes, defaultScope) + } + } + if len(scopes) != 0 { + queries.Add(QueryKeyScope, strings.Join(scopes, " ")) } - - queries.Add("scope", strings.Join(scopes, " ")) resources := option.Resources if slices.Contains(scopes, UserScopeOrganizations) { @@ -46,16 +60,39 @@ func GenerateSignInUri(option *SignInUriGenerationOptions) (string, error) { if len(resources) != 0 { for _, resource := range resources { - queries.Add("resource", resource) + queries.Add(QueryKeyResource, resource) } } - queries.Add("response_type", "code") + queries.Add(QueryKeyResponseType, ResponseTypeCode) if option.Prompt != "" { - queries.Add("prompt", option.Prompt) + queries.Add(QueryKeyPrompt, option.Prompt) } else { - queries.Add("prompt", "consent") + queries.Add(QueryKeyPrompt, PromptConsent) + } + + if option.LoginHint != "" { + queries.Add(QueryKeyLoginHint, option.LoginHint) + } + + if option.FirstScreen != "" { + queries.Add(QueryKeyFirstScreen, option.FirstScreen) + } + + if len(option.Identifiers) != 0 { + queries.Add(QueryKeyIdentifiers, strings.Join(option.Identifiers, " ")) + } + + if option.DirectSignIn != nil { + directSignInValue := option.DirectSignIn.Method + ":" + option.DirectSignIn.Target + queries.Add(QueryKeyDirectSignIn, directSignInValue) + } + + if len(option.ExtraParams) != 0 { + for key, value := range option.ExtraParams { + queries.Add(key, value) + } } unescapedQueries, unescapeQueryErr := url.QueryUnescape(queries.Encode()) diff --git a/gin-sample/main.go b/gin-sample/main.go index 123195c..9e52c4a 100644 --- a/gin-sample/main.go +++ b/gin-sample/main.go @@ -58,7 +58,7 @@ func main() { router.GET("/sign-in", func(ctx *gin.Context) { session := sessions.Default(ctx) logtoClient := client.NewLogtoClient(logtoConfig, &SessionStorage{session: session}) - signInUri, err := logtoClient.SignIn(os.Getenv("REDIRECT_URI")) + signInUri, err := logtoClient.SignInWithRedirectUri(os.Getenv("REDIRECT_URI")) if err != nil { ctx.String(http.StatusInternalServerError, err.Error()) return