feat: add refresh token mechanism for server side (#336)

* feat: add refresh token mechanism for server side

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>

* feat: add refresh token expire configuration UI

Signed-off-by: Yixiang Zhao <seriouszyx@foxmail.com>
This commit is contained in:
Yixiang Zhao 2021-12-18 18:49:38 +08:00 committed by GitHub
parent 95f2a3b311
commit 755d912f61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 140 additions and 22 deletions

View File

@ -165,3 +165,24 @@ func (c *ApiController) GetOAuthToken() {
c.Data["json"] = object.GetOAuthToken(grantType, clientId, clientSecret, code) c.Data["json"] = object.GetOAuthToken(grantType, clientId, clientSecret, code)
c.ServeJSON() c.ServeJSON()
} }
// RefreshToken
// @Title RefreshToken
// @Description refresh OAuth access token
// @Param grant_type query string true "OAuth grant type"
// @Param refresh_token query string true "OAuth refresh token"
// @Param scope query string true "OAuth scope"
// @Param client_id query string true "OAuth client id"
// @Param client_secret query string true "OAuth client secret"
// @Success 200 {object} object.TokenWrapper The Response object
// @router /login/oauth/refresh_token [post]
func (c *ApiController) RefreshToken() {
grantType := c.Input().Get("grant_type")
refreshToken := c.Input().Get("refresh_token")
scope := c.Input().Get("scope")
clientId := c.Input().Get("client_id")
clientSecret := c.Input().Get("client_secret")
c.Data["json"] = object.RefreshToken(grantType, refreshToken, scope, clientId, clientSecret)
c.ServeJSON()
}

View File

@ -36,18 +36,19 @@ type Application struct {
SignupItems []*SignupItem `xorm:"varchar(1000)" json:"signupItems"` SignupItems []*SignupItem `xorm:"varchar(1000)" json:"signupItems"`
OrganizationObj *Organization `xorm:"-" json:"organizationObj"` OrganizationObj *Organization `xorm:"-" json:"organizationObj"`
ClientId string `xorm:"varchar(100)" json:"clientId"` ClientId string `xorm:"varchar(100)" json:"clientId"`
ClientSecret string `xorm:"varchar(100)" json:"clientSecret"` ClientSecret string `xorm:"varchar(100)" json:"clientSecret"`
RedirectUris []string `xorm:"varchar(1000)" json:"redirectUris"` RedirectUris []string `xorm:"varchar(1000)" json:"redirectUris"`
TokenFormat string `xorm:"varchar(100)" json:"tokenFormat"` TokenFormat string `xorm:"varchar(100)" json:"tokenFormat"`
ExpireInHours int `json:"expireInHours"` ExpireInHours int `json:"expireInHours"`
SignupUrl string `xorm:"varchar(200)" json:"signupUrl"` RefreshExpireInHours int `json:"refreshExpireInHours"`
SigninUrl string `xorm:"varchar(200)" json:"signinUrl"` SignupUrl string `xorm:"varchar(200)" json:"signupUrl"`
ForgetUrl string `xorm:"varchar(200)" json:"forgetUrl"` SigninUrl string `xorm:"varchar(200)" json:"signinUrl"`
AffiliationUrl string `xorm:"varchar(100)" json:"affiliationUrl"` ForgetUrl string `xorm:"varchar(200)" json:"forgetUrl"`
TermsOfUse string `xorm:"varchar(100)" json:"termsOfUse"` AffiliationUrl string `xorm:"varchar(100)" json:"affiliationUrl"`
SignupHtml string `xorm:"mediumtext" json:"signupHtml"` TermsOfUse string `xorm:"varchar(100)" json:"termsOfUse"`
SigninHtml string `xorm:"mediumtext" json:"signinHtml"` SignupHtml string `xorm:"mediumtext" json:"signupHtml"`
SigninHtml string `xorm:"mediumtext" json:"signinHtml"`
} }
func GetApplicationCount(owner string) int { func GetApplicationCount(owner string) int {

View File

@ -17,6 +17,7 @@ package object
import ( import (
"fmt" "fmt"
"strings" "strings"
"time"
"github.com/casbin/casdoor/util" "github.com/casbin/casdoor/util"
"xorm.io/core" "xorm.io/core"
@ -36,11 +37,12 @@ type Token struct {
Organization string `xorm:"varchar(100)" json:"organization"` Organization string `xorm:"varchar(100)" json:"organization"`
User string `xorm:"varchar(100)" json:"user"` User string `xorm:"varchar(100)" json:"user"`
Code string `xorm:"varchar(100)" json:"code"` Code string `xorm:"varchar(100)" json:"code"`
AccessToken string `xorm:"mediumtext" json:"accessToken"` AccessToken string `xorm:"mediumtext" json:"accessToken"`
ExpiresIn int `json:"expiresIn"` RefreshToken string `xorm:"mediumtext" json:"refreshToken"`
Scope string `xorm:"varchar(100)" json:"scope"` ExpiresIn int `json:"expiresIn"`
TokenType string `xorm:"varchar(100)" json:"tokenType"` Scope string `xorm:"varchar(100)" json:"scope"`
TokenType string `xorm:"varchar(100)" json:"tokenType"`
} }
type TokenWrapper struct { type TokenWrapper struct {
@ -192,7 +194,7 @@ func GetOAuthCode(userId string, clientId string, responseType string, redirectU
} }
} }
accessToken, err := generateJwtToken(application, user, nonce) accessToken, refreshToken, err := generateJwtToken(application, user, nonce)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -206,6 +208,7 @@ func GetOAuthCode(userId string, clientId string, responseType string, redirectU
User: user.Name, User: user.Name,
Code: util.GenerateClientId(), Code: util.GenerateClientId(),
AccessToken: accessToken, AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresIn: application.ExpireInHours * 60, ExpiresIn: application.ExpireInHours * 60,
Scope: scope, Scope: scope,
TokenType: "Bearer", TokenType: "Bearer",
@ -285,3 +288,75 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code
return tokenWrapper return tokenWrapper
} }
func RefreshToken(grantType string, refreshToken string, scope string, clientId string, clientSecret string) *Code {
// check parameters
if grantType != "refresh_token" {
return &Code{
Message: "error: grant_type should be \"refresh_token\"",
Code: "",
}
}
application := GetApplicationByClientId(clientId)
if application == nil {
return &Code{
Message: "error: invalid client_id",
Code: "",
}
}
if application.ClientSecret != clientSecret {
return &Code{
Message: "error: invalid client_secret",
Code: "",
}
}
// check whether the refresh token is valid, and has not expired.
token := Token{RefreshToken: refreshToken}
existed, err := adapter.Engine.Get(&token)
if err != nil || !existed {
return &Code{
Message: "error: invalid refresh_token",
Code: "",
}
}
claims, err := ParseJwtToken(refreshToken)
if err != nil {
return &Code{
Message: "error: invalid refresh_token",
Code: "",
}
}
if time.Now().Unix() > claims.ExpiresAt.Unix() {
return &Code{
Message: "error: expired refresh_token",
Code: "",
}
}
// generate a new token
user := getUser(application.Owner, token.User)
newAccessToken, newRefreshToken, err := generateJwtToken(application, user, "")
if err != nil {
panic(err)
}
newToken := &Token{
Owner: application.Owner,
Name: util.GenerateId(),
CreatedTime: util.GetCurrentTime(),
Application: application.Name,
Organization: user.Owner,
User: user.Name,
Code: util.GenerateClientId(),
AccessToken: newAccessToken,
RefreshToken: newRefreshToken,
ExpiresIn: application.ExpireInHours * 60,
Scope: scope,
TokenType: "Bearer",
}
AddToken(newToken)
return &Code{
Message: "",
Code: token.Code,
}
}

View File

@ -35,9 +35,10 @@ type Claims struct {
jwt.RegisteredClaims jwt.RegisteredClaims
} }
func generateJwtToken(application *Application, user *User, nonce string) (string, error) { func generateJwtToken(application *Application, user *User, nonce string) (string, string, error) {
nowTime := time.Now() nowTime := time.Now()
expireTime := nowTime.Add(time.Duration(application.ExpireInHours) * time.Hour) expireTime := nowTime.Add(time.Duration(application.ExpireInHours) * time.Hour)
refreshExpireTime := nowTime.Add(time.Duration(application.RefreshExpireInHours) * time.Hour)
user.Password = "" user.Password = ""
@ -60,17 +61,23 @@ func generateJwtToken(application *Application, user *User, nonce string) (strin
} }
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
claims.ExpiresAt = jwt.NewNumericDate(refreshExpireTime)
refreshToken := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
// Use "token_jwt_key.key" as RSA private key // Use "token_jwt_key.key" as RSA private key
privateKey := tokenJwtPrivateKey privateKey := tokenJwtPrivateKey
key, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(privateKey)) key, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(privateKey))
if err != nil { if err != nil {
return "", err return "", "", err
} }
tokenString, err := token.SignedString(key) tokenString, err := token.SignedString(key)
if err != nil {
return "", "", err
}
refreshTokenString, err := refreshToken.SignedString(key)
return tokenString, err return tokenString, refreshTokenString, err
} }
func ParseJwtToken(token string) (*Claims, error) { func ParseJwtToken(token string) (*Claims, error) {

View File

@ -16,6 +16,7 @@ package routers
import ( import (
"fmt" "fmt"
"time"
"github.com/astaxie/beego/context" "github.com/astaxie/beego/context"
"github.com/casbin/casdoor/object" "github.com/casbin/casdoor/object"
@ -35,6 +36,9 @@ func AutoSigninFilter(ctx *context.Context) {
responseError(ctx, "invalid JWT token") responseError(ctx, "invalid JWT token")
return return
} }
if time.Now().Unix() > claims.ExpiresAt.Unix() {
responseError(ctx, "expired JWT token")
}
userId := fmt.Sprintf("%s/%s", claims.User.Owner, claims.User.Name) userId := fmt.Sprintf("%s/%s", claims.User.Owner, claims.User.Name)
setSessionUser(ctx, userId) setSessionUser(ctx, userId)

View File

@ -82,7 +82,7 @@ class ApplicationEditPage extends React.Component {
} }
parseApplicationField(key, value) { parseApplicationField(key, value) {
if (["expireInHours"].includes(key)) { if (["expireInHours"].includes(key) || ["refreshExpireInHours"].includes(key)) {
value = Setting.myParseInt(value); value = Setting.myParseInt(value);
} }
return value; return value;
@ -261,6 +261,16 @@ class ApplicationEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Refresh token expire"), i18next.t("general:Refresh token expire - Tooltip"))} :
</Col>
<Col span={22} >
<Input style={{width: "150px"}} value={this.state.application.refreshExpireInHours} suffix="Hours" onChange={e => {
this.updateApplicationField('refreshExpireInHours', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 19 : 2}> <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 19 : 2}>
{Setting.getLabel(i18next.t("application:Password ON"), i18next.t("application:Password ON - Tooltip"))} : {Setting.getLabel(i18next.t("application:Password ON"), i18next.t("application:Password ON - Tooltip"))} :