mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-03 20:50:19 +08:00
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:
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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"`
|
||||||
|
188
object/token.go
188
object/token.go
@ -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
|
||||||
|
}
|
||||||
|
@ -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 != "" {
|
||||||
|
@ -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"))} :
|
||||||
|
Reference in New Issue
Block a user