diff --git a/controllers/token.go b/controllers/token.go index 427f2e90..5f13db13 100644 --- a/controllers/token.go +++ b/controllers/token.go @@ -171,12 +171,16 @@ func (c *ApiController) GetOAuthToken() { clientSecret := c.Input().Get("client_secret") code := c.Input().Get("code") verifier := c.Input().Get("code_verifier") + scope := c.Input().Get("scope") + username := c.Input().Get("username") + password := c.Input().Get("password") if clientId == "" && clientSecret == "" { clientId, clientSecret, _ = c.Ctx.Request.BasicAuth() } + host := c.Ctx.Request.Host - c.Data["json"] = object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier) + c.Data["json"] = object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, username, password, host) c.ServeJSON() } diff --git a/object/application.go b/object/application.go index c80c08df..61e4548a 100644 --- a/object/application.go +++ b/object/application.go @@ -38,6 +38,7 @@ type Application struct { EnableCodeSignin bool `json:"enableCodeSignin"` Providers []*ProviderItem `xorm:"mediumtext" json:"providers"` SignupItems []*SignupItem `xorm:"varchar(1000)" json:"signupItems"` + GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"` OrganizationObj *Organization `xorm:"-" json:"organizationObj"` ClientId string `xorm:"varchar(100)" json:"clientId"` diff --git a/object/token.go b/object/token.go index f6b581c3..05d248ca 100644 --- a/object/token.go +++ b/object/token.go @@ -17,6 +17,7 @@ package object import ( "crypto/sha256" "encoding/base64" + "errors" "fmt" "strings" "time" @@ -261,7 +262,7 @@ func GetOAuthCode(userId string, clientId string, responseType string, redirectU } } -func GetOAuthToken(grantType string, clientId string, clientSecret string, code string, verifier string) *TokenWrapper { +func GetOAuthToken(grantType string, clientId string, clientSecret string, code string, verifier string, scope string, username string, password string, host string) *TokenWrapper { application := GetApplicationByClientId(clientId) if application == nil { return &TokenWrapper{ @@ -272,75 +273,30 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code } } - if grantType != "authorization_code" { + //Check if grantType is allowed in the current application + if !isGrantTypeValid(grantType, application.GrantTypes) { return &TokenWrapper{ - AccessToken: "error: grant_type should be \"authorization_code\"", + AccessToken: fmt.Sprintf("error: grant_type: %s is not supported in this application", grantType), TokenType: "", ExpiresIn: 0, Scope: "", } } - if code == "" { - return &TokenWrapper{ - AccessToken: "error: authorization code should not be empty", - TokenType: "", - ExpiresIn: 0, - Scope: "", - } + var token *Token + var err error + switch grantType { + case "authorization_code": // Authorization Code Grant + token, err = GetAuthorizationCodeToken(application, clientSecret, code, verifier) + case "password": // Resource Owner Password Credentials Grant + token, err = GetPasswordToken(application, username, password, scope, host) + case "client_credentials": // Client Credentials Grant + token, err = GetClientCredentialsToken(application, clientSecret, scope, host) } - token := getTokenByCode(code) - if token == nil { + if err != nil { return &TokenWrapper{ - AccessToken: "error: invalid authorization code", - TokenType: "", - ExpiresIn: 0, - Scope: "", - } - } - - if application.Name != token.Application { - return &TokenWrapper{ - AccessToken: "error: the token is for wrong application (client_id)", - TokenType: "", - ExpiresIn: 0, - Scope: "", - } - } - - if application.ClientSecret != clientSecret { - return &TokenWrapper{ - AccessToken: "error: invalid client_secret", - TokenType: "", - ExpiresIn: 0, - Scope: "", - } - } - - if token.CodeChallenge != "" && pkceChallenge(verifier) != token.CodeChallenge { - return &TokenWrapper{ - AccessToken: "error: incorrect code_verifier", - TokenType: "", - ExpiresIn: 0, - Scope: "", - } - } - - if token.CodeIsUsed { - // anti replay attacks - return &TokenWrapper{ - AccessToken: "error: authorization code has been used", - TokenType: "", - ExpiresIn: 0, - Scope: "", - } - } - - if time.Now().Unix() > token.CodeExpireIn { - // code must be used within 5 minutes - return &TokenWrapper{ - AccessToken: "error: authorization code has expired", + AccessToken: err.Error(), TokenType: "", ExpiresIn: 0, Scope: "", @@ -459,3 +415,115 @@ func pkceChallenge(verifier string) string { challenge := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(sum[:]) return challenge } + +// Check if grantType is allowed in the current application +// authorization_code is allowed by default +func isGrantTypeValid(method string, grantTypes []string) bool { + if method == "authorization_code" { + return true + } + for _, m := range grantTypes { + if m == method { + return true + } + } + return false +} + +// Authorization code flow +func GetAuthorizationCodeToken(application *Application, clientSecret string, code string, verifier string) (*Token, error) { + if code == "" { + return nil, errors.New("error: authorization code should not be empty") + } + + token := getTokenByCode(code) + if token == nil { + return nil, errors.New("error: invalid authorization code") + } + if token.CodeIsUsed { + // anti replay attacks + return nil, errors.New("error: authorization code has been used") + } + if application.ClientSecret != clientSecret { + return nil, errors.New("error: invalid client_secret") + } + + if application.Name != token.Application { + return nil, errors.New("error: the token is for wrong application (client_id)") + } + + if token.CodeChallenge != "" && pkceChallenge(verifier) != token.CodeChallenge { + return nil, errors.New("error: incorrect code_verifier") + } + + if time.Now().Unix() > token.CodeExpireIn { + // code must be used within 5 minutes + return nil, errors.New("error: authorization code has expired") + } + return token, nil +} + +// Resource Owner Password Credentials flow +func GetPasswordToken(application *Application, username string, password string, scope string, host string) (*Token, error) { + user := getUser(application.Organization, username) + if user == nil { + return nil, errors.New("error: the user does not exist") + } + if user.Password != password { + return nil, errors.New("error: invalid username or password") + } + if user.IsForbidden { + return nil, errors.New("error: the user is forbidden to sign in, please contact the administrator") + } + accessToken, refreshToken, err := generateJwtToken(application, user, "", scope, host) + if err != nil { + return nil, err + } + token := &Token{ + Owner: application.Owner, + Name: util.GenerateId(), + CreatedTime: util.GetCurrentTime(), + Application: application.Name, + Organization: user.Owner, + User: user.Name, + Code: util.GenerateClientId(), + AccessToken: accessToken, + RefreshToken: refreshToken, + ExpiresIn: application.ExpireInHours * 60, + Scope: scope, + TokenType: "Bearer", + CodeIsUsed: true, + } + AddToken(token) + return token, nil +} + +// Client Credentials flow +func GetClientCredentialsToken(application *Application, clientSecret string, scope string, host string) (*Token, error) { + if application.ClientSecret != clientSecret { + return nil, errors.New("error: invalid client_secret") + } + nullUser := &User{ + Name: fmt.Sprintf("app/%s", application.Name), + } + accessToken, _, err := generateJwtToken(application, nullUser, "", scope, host) + if err != nil { + return nil, err + } + token := &Token{ + Owner: application.Owner, + Name: util.GenerateId(), + CreatedTime: util.GetCurrentTime(), + Application: application.Name, + Organization: application.Organization, + User: nullUser.Name, + Code: util.GenerateClientId(), + AccessToken: accessToken, + ExpiresIn: application.ExpireInHours * 60, + Scope: scope, + TokenType: "Bearer", + CodeIsUsed: true, + } + AddToken(token) + return token, nil +} diff --git a/routers/auto_signin_filter.go b/routers/auto_signin_filter.go index 1e807bb3..65ac9c96 100644 --- a/routers/auto_signin_filter.go +++ b/routers/auto_signin_filter.go @@ -62,7 +62,7 @@ func AutoSigninFilter(ctx *context.Context) { // "/page?username=abc&password=123" userId = ctx.Input.Query("username") password := ctx.Input.Query("password") - if userId != "" && password != "" { + if userId != "" && password != "" && ctx.Input.Query("grant_type") == "" { owner, name := util.GetOwnerAndNameFromId(userId) _, msg := object.CheckUserPassword(owner, name, password) if msg != "" { diff --git a/web/src/ApplicationEditPage.js b/web/src/ApplicationEditPage.js index 5936a435..7437ff91 100644 --- a/web/src/ApplicationEditPage.js +++ b/web/src/ApplicationEditPage.js @@ -61,6 +61,9 @@ class ApplicationEditPage extends React.Component { getApplication() { ApplicationBackend.getApplication("admin", this.state.applicationName) .then((application) => { + if (application.grantTypes === null || application.grantTypes.length === 0) { + application.grantTypes = ["authorization_code"]; + } this.setState({ application: application, }); @@ -435,6 +438,27 @@ class ApplicationEditPage extends React.Component { + + + {Setting.getLabel(i18next.t("application:Grant Types"), i18next.t("application:Grant Types - Tooltip"))} : + + + + + + {Setting.getLabel(i18next.t("general:Providers"), i18next.t("general:Providers - Tooltip"))} :