feat: add two authentication flow types (#512)

* feat: add two authentication flow types

Signed-off-by: Steve0x2a <stevesough@gmail.com>

* fix: delete implicit method

Signed-off-by: Steve0x2a <stevesough@gmail.com>

* fix: use a more appropriate name

Signed-off-by: Steve0x2a <stevesough@gmail.com>

* fix: apply suggestion

Signed-off-by: Steve0x2a <stevesough@gmail.com>

* fix: remove redundant code

Signed-off-by: Steve0x2a <stevesough@gmail.com>
This commit is contained in:
Steve0x2a
2022-02-27 14:05:07 +08:00
committed by GitHub
parent 21392dcc14
commit 2c97f8a8b7
5 changed files with 159 additions and 62 deletions

View File

@ -171,12 +171,16 @@ func (c *ApiController) GetOAuthToken() {
clientSecret := c.Input().Get("client_secret") clientSecret := c.Input().Get("client_secret")
code := c.Input().Get("code") code := c.Input().Get("code")
verifier := c.Input().Get("code_verifier") verifier := c.Input().Get("code_verifier")
scope := c.Input().Get("scope")
username := c.Input().Get("username")
password := c.Input().Get("password")
if clientId == "" && clientSecret == "" { if clientId == "" && clientSecret == "" {
clientId, clientSecret, _ = c.Ctx.Request.BasicAuth() 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() c.ServeJSON()
} }

View File

@ -38,6 +38,7 @@ type Application struct {
EnableCodeSignin bool `json:"enableCodeSignin"` EnableCodeSignin bool `json:"enableCodeSignin"`
Providers []*ProviderItem `xorm:"mediumtext" json:"providers"` Providers []*ProviderItem `xorm:"mediumtext" json:"providers"`
SignupItems []*SignupItem `xorm:"varchar(1000)" json:"signupItems"` SignupItems []*SignupItem `xorm:"varchar(1000)" json:"signupItems"`
GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"`
OrganizationObj *Organization `xorm:"-" json:"organizationObj"` OrganizationObj *Organization `xorm:"-" json:"organizationObj"`
ClientId string `xorm:"varchar(100)" json:"clientId"` ClientId string `xorm:"varchar(100)" json:"clientId"`

View File

@ -17,6 +17,7 @@ package object
import ( import (
"crypto/sha256" "crypto/sha256"
"encoding/base64" "encoding/base64"
"errors"
"fmt" "fmt"
"strings" "strings"
"time" "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) application := GetApplicationByClientId(clientId)
if application == nil { if application == nil {
return &TokenWrapper{ 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{ 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: "", TokenType: "",
ExpiresIn: 0, ExpiresIn: 0,
Scope: "", Scope: "",
} }
} }
if code == "" { var token *Token
return &TokenWrapper{ var err error
AccessToken: "error: authorization code should not be empty", switch grantType {
TokenType: "", case "authorization_code": // Authorization Code Grant
ExpiresIn: 0, token, err = GetAuthorizationCodeToken(application, clientSecret, code, verifier)
Scope: "", 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 err != nil {
if token == nil {
return &TokenWrapper{ return &TokenWrapper{
AccessToken: "error: invalid authorization code", AccessToken: err.Error(),
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",
TokenType: "", TokenType: "",
ExpiresIn: 0, ExpiresIn: 0,
Scope: "", Scope: "",
@ -459,3 +415,115 @@ func pkceChallenge(verifier string) string {
challenge := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(sum[:]) challenge := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(sum[:])
return challenge 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
}

View File

@ -62,7 +62,7 @@ func AutoSigninFilter(ctx *context.Context) {
// "/page?username=abc&password=123" // "/page?username=abc&password=123"
userId = ctx.Input.Query("username") userId = ctx.Input.Query("username")
password := ctx.Input.Query("password") password := ctx.Input.Query("password")
if userId != "" && password != "" { if userId != "" && password != "" && ctx.Input.Query("grant_type") == "" {
owner, name := util.GetOwnerAndNameFromId(userId) owner, name := util.GetOwnerAndNameFromId(userId)
_, msg := object.CheckUserPassword(owner, name, password) _, msg := object.CheckUserPassword(owner, name, password)
if msg != "" { if msg != "" {

View File

@ -61,6 +61,9 @@ class ApplicationEditPage extends React.Component {
getApplication() { getApplication() {
ApplicationBackend.getApplication("admin", this.state.applicationName) ApplicationBackend.getApplication("admin", this.state.applicationName)
.then((application) => { .then((application) => {
if (application.grantTypes === null || application.grantTypes.length === 0) {
application.grantTypes = ["authorization_code"];
}
this.setState({ this.setState({
application: application, application: application,
}); });
@ -435,6 +438,27 @@ class ApplicationEditPage extends React.Component {
</Popover> </Popover>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("application:Grant Types"), i18next.t("application:Grant Types - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} mode="tags" style={{width: '100%'}}
value={this.state.application.grantTypes}
onChange={(value => {
this.updateApplicationField('grantTypes', value);
})} >
{
[
{id: "authorization_code", name: "Authorization Code"},
{id: "password", name: "Password"},
{id: "client_credentials", name: "Client Credentials"},
].map((item, index)=><Option key={index} value={item.id}>{item.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Providers"), i18next.t("general:Providers - Tooltip"))} : {Setting.getLabel(i18next.t("general:Providers"), i18next.t("general:Providers - Tooltip"))} :