mirror of
https://github.com/casdoor/casdoor.git
synced 2025-05-23 18:54:03 +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.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()
|
||||||
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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)
|
||||||
|
@ -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"))} :
|
||||||
|
Loading…
x
Reference in New Issue
Block a user