mirror of
https://github.com/casdoor/casdoor.git
synced 2025-05-23 02:35:49 +08:00
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:
parent
95f2a3b311
commit
755d912f61
@ -165,3 +165,24 @@ func (c *ApiController) GetOAuthToken() {
|
||||
c.Data["json"] = object.GetOAuthToken(grantType, clientId, clientSecret, code)
|
||||
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()
|
||||
}
|
||||
|
@ -36,18 +36,19 @@ type Application struct {
|
||||
SignupItems []*SignupItem `xorm:"varchar(1000)" json:"signupItems"`
|
||||
OrganizationObj *Organization `xorm:"-" json:"organizationObj"`
|
||||
|
||||
ClientId string `xorm:"varchar(100)" json:"clientId"`
|
||||
ClientSecret string `xorm:"varchar(100)" json:"clientSecret"`
|
||||
RedirectUris []string `xorm:"varchar(1000)" json:"redirectUris"`
|
||||
TokenFormat string `xorm:"varchar(100)" json:"tokenFormat"`
|
||||
ExpireInHours int `json:"expireInHours"`
|
||||
SignupUrl string `xorm:"varchar(200)" json:"signupUrl"`
|
||||
SigninUrl string `xorm:"varchar(200)" json:"signinUrl"`
|
||||
ForgetUrl string `xorm:"varchar(200)" json:"forgetUrl"`
|
||||
AffiliationUrl string `xorm:"varchar(100)" json:"affiliationUrl"`
|
||||
TermsOfUse string `xorm:"varchar(100)" json:"termsOfUse"`
|
||||
SignupHtml string `xorm:"mediumtext" json:"signupHtml"`
|
||||
SigninHtml string `xorm:"mediumtext" json:"signinHtml"`
|
||||
ClientId string `xorm:"varchar(100)" json:"clientId"`
|
||||
ClientSecret string `xorm:"varchar(100)" json:"clientSecret"`
|
||||
RedirectUris []string `xorm:"varchar(1000)" json:"redirectUris"`
|
||||
TokenFormat string `xorm:"varchar(100)" json:"tokenFormat"`
|
||||
ExpireInHours int `json:"expireInHours"`
|
||||
RefreshExpireInHours int `json:"refreshExpireInHours"`
|
||||
SignupUrl string `xorm:"varchar(200)" json:"signupUrl"`
|
||||
SigninUrl string `xorm:"varchar(200)" json:"signinUrl"`
|
||||
ForgetUrl string `xorm:"varchar(200)" json:"forgetUrl"`
|
||||
AffiliationUrl string `xorm:"varchar(100)" json:"affiliationUrl"`
|
||||
TermsOfUse string `xorm:"varchar(100)" json:"termsOfUse"`
|
||||
SignupHtml string `xorm:"mediumtext" json:"signupHtml"`
|
||||
SigninHtml string `xorm:"mediumtext" json:"signinHtml"`
|
||||
}
|
||||
|
||||
func GetApplicationCount(owner string) int {
|
||||
|
@ -17,6 +17,7 @@ package object
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/casbin/casdoor/util"
|
||||
"xorm.io/core"
|
||||
@ -36,11 +37,12 @@ type Token struct {
|
||||
Organization string `xorm:"varchar(100)" json:"organization"`
|
||||
User string `xorm:"varchar(100)" json:"user"`
|
||||
|
||||
Code string `xorm:"varchar(100)" json:"code"`
|
||||
AccessToken string `xorm:"mediumtext" json:"accessToken"`
|
||||
ExpiresIn int `json:"expiresIn"`
|
||||
Scope string `xorm:"varchar(100)" json:"scope"`
|
||||
TokenType string `xorm:"varchar(100)" json:"tokenType"`
|
||||
Code string `xorm:"varchar(100)" json:"code"`
|
||||
AccessToken string `xorm:"mediumtext" json:"accessToken"`
|
||||
RefreshToken string `xorm:"mediumtext" json:"refreshToken"`
|
||||
ExpiresIn int `json:"expiresIn"`
|
||||
Scope string `xorm:"varchar(100)" json:"scope"`
|
||||
TokenType string `xorm:"varchar(100)" json:"tokenType"`
|
||||
}
|
||||
|
||||
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 {
|
||||
panic(err)
|
||||
}
|
||||
@ -206,6 +208,7 @@ func GetOAuthCode(userId string, clientId string, responseType string, redirectU
|
||||
User: user.Name,
|
||||
Code: util.GenerateClientId(),
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: application.ExpireInHours * 60,
|
||||
Scope: scope,
|
||||
TokenType: "Bearer",
|
||||
@ -285,3 +288,75 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
@ -35,9 +35,10 @@ type Claims struct {
|
||||
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()
|
||||
expireTime := nowTime.Add(time.Duration(application.ExpireInHours) * time.Hour)
|
||||
refreshExpireTime := nowTime.Add(time.Duration(application.RefreshExpireInHours) * time.Hour)
|
||||
|
||||
user.Password = ""
|
||||
|
||||
@ -60,17 +61,23 @@ func generateJwtToken(application *Application, user *User, nonce string) (strin
|
||||
}
|
||||
|
||||
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
|
||||
privateKey := tokenJwtPrivateKey
|
||||
key, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(privateKey))
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -16,6 +16,7 @@ package routers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/astaxie/beego/context"
|
||||
"github.com/casbin/casdoor/object"
|
||||
@ -35,6 +36,9 @@ func AutoSigninFilter(ctx *context.Context) {
|
||||
responseError(ctx, "invalid JWT token")
|
||||
return
|
||||
}
|
||||
if time.Now().Unix() > claims.ExpiresAt.Unix() {
|
||||
responseError(ctx, "expired JWT token")
|
||||
}
|
||||
|
||||
userId := fmt.Sprintf("%s/%s", claims.User.Owner, claims.User.Name)
|
||||
setSessionUser(ctx, userId)
|
||||
|
@ -82,7 +82,7 @@ class ApplicationEditPage extends React.Component {
|
||||
}
|
||||
|
||||
parseApplicationField(key, value) {
|
||||
if (["expireInHours"].includes(key)) {
|
||||
if (["expireInHours"].includes(key) || ["refreshExpireInHours"].includes(key)) {
|
||||
value = Setting.myParseInt(value);
|
||||
}
|
||||
return value;
|
||||
@ -261,6 +261,16 @@ class ApplicationEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</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'}} >
|
||||
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("application:Password ON"), i18next.t("application:Password ON - Tooltip"))} :
|
||||
|
Loading…
x
Reference in New Issue
Block a user