This is an automated email from the ASF dual-hosted git repository.

likyh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git


The following commit(s) were added to refs/heads/main by this push:
     new 3281ba053 Refresh token (#5174)
3281ba053 is described below

commit 3281ba05336c8ee2efe05725a6a128897748e195
Author: Klesh Wong <[email protected]>
AuthorDate: Mon May 15 16:19:53 2023 +0800

    Refresh token (#5174)
    
    * feat: BE supports token refreshing
    
    * feat: FE supports token refreshing
    
    * fix: make swag failed
    
    * fix: remove catch to avoid unwanted behvior
---
 backend/server/api/api.go               |  1 +
 backend/server/api/login/login.go       | 28 ++++++++++++++++++++++++--
 backend/server/services/auth/auth.go    |  5 +++++
 backend/server/services/auth/cognito.go | 32 +++++++++++++++++++++++++-----
 config-ui/src/pages/login/login.tsx     |  1 +
 config-ui/src/utils/request.ts          | 35 +++++++++++++++++++++++++++++++++
 6 files changed, 95 insertions(+), 7 deletions(-)

diff --git a/backend/server/api/api.go b/backend/server/api/api.go
index c9bf7d93e..be830bb10 100644
--- a/backend/server/api/api.go
+++ b/backend/server/api/api.go
@@ -74,6 +74,7 @@ func CreateApiService() {
                // Add login endpoint
                router.POST("/login", login.Login)
                router.POST("/login/newpassword", login.NewPassword)
+               router.POST("/login/refreshtoken", login.RefreshToken)
                // Use AuthenticationMiddleware for protected routes
                router.Use(auth.Middleware)
        }
diff --git a/backend/server/api/login/login.go 
b/backend/server/api/login/login.go
index 6b71ef41d..7ef7f6d45 100644
--- a/backend/server/api/login/login.go
+++ b/backend/server/api/login/login.go
@@ -32,7 +32,7 @@ import (
 // @Tags framework/login
 // @Accept application/json
 // @Param login body auth.LoginRequest true "json"
-// @Success 200  {object} LoginResponse
+// @Success 200  {object} auth.LoginResponse
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /login [post]
@@ -63,7 +63,7 @@ func Login(ctx *gin.Context) {
 // @Tags framework/NewPassword
 // @Accept application/json
 // @Param newpassword body auth.NewPasswordRequest true "json"
-// @Success 200  {object} shared.ApiBody
+// @Success 200  {object} auth.LoginResponse
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /password [post]
@@ -81,3 +81,27 @@ func NewPassword(ctx *gin.Context) {
        }
        shared.ApiOutputSuccess(ctx, res, http.StatusOK)
 }
+
+// @Summary post RefreshToken
+// @Description post RefreshToken
+// @Tags framework/RefreshToken
+// @Accept application/json
+// @Param refreshtoken body auth.RefreshTokenRequest true "json"
+// @Success 200  {object} auth.LoginResponse
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /password [post]
+func RefreshToken(ctx *gin.Context) {
+       req := &auth.RefreshTokenRequest{}
+       err := ctx.ShouldBind(req)
+       if err != nil {
+               shared.ApiOutputError(ctx, errors.BadInput.Wrap(err, 
shared.BadRequestBody))
+               return
+       }
+       res, err := auth.Provider.RefreshToken(req)
+       if err != nil {
+               shared.ApiOutputError(ctx, errors.BadInput.Wrap(err, "failed to 
refresh token"))
+               return
+       }
+       shared.ApiOutputSuccess(ctx, res, http.StatusOK)
+}
diff --git a/backend/server/services/auth/auth.go 
b/backend/server/services/auth/auth.go
index 742560dc6..fe839e2d3 100644
--- a/backend/server/services/auth/auth.go
+++ b/backend/server/services/auth/auth.go
@@ -55,10 +55,15 @@ type NewPasswordRequest struct {
        Session     string `json:"session"`
 }
 
+type RefreshTokenRequest struct {
+       RefreshToken string `json:"refreshToken"`
+}
+
 // auth provider interface
 type AuthProvider interface {
        SignIn(*LoginRequest) (*LoginResponse, errors.Error)
        NewPassword(*NewPasswordRequest) (*LoginResponse, errors.Error)
+       RefreshToken(*RefreshTokenRequest) (*LoginResponse, errors.Error)
        // ChangePassword(ctx *gin.Context, oldPassword, newPassword string) 
errors.Error
        CheckAuth(token string) (*jwt.Token, errors.Error)
 }
diff --git a/backend/server/services/auth/cognito.go 
b/backend/server/services/auth/cognito.go
index ac9f24d7e..37c13f6ae 100644
--- a/backend/server/services/auth/cognito.go
+++ b/backend/server/services/auth/cognito.go
@@ -113,11 +113,14 @@ func (cgt *AwsCognitoProvider) SignIn(loginReq 
*LoginRequest) (*LoginResponse, e
        }
 
        // Call Cognito to get auth tokens
+       return cgt.initiateAuth(input)
+}
+
+func (cgt *AwsCognitoProvider) initiateAuth(input 
*cognitoidentityprovider.InitiateAuthInput) (*LoginResponse, errors.Error) {
        response, err := cgt.client.InitiateAuth(input)
        if err != nil {
                return nil, errors.BadInput.New(err.Error())
        }
-
        loginRes := &LoginResponse{
                ChallengeName:       response.ChallengeName,
                ChallengeParameters: response.ChallengeParameters,
@@ -132,7 +135,6 @@ func (cgt *AwsCognitoProvider) SignIn(loginReq 
*LoginRequest) (*LoginResponse, e
                        TokenType:    response.AuthenticationResult.TokenType,
                }
        }
-
        return loginRes, nil
 }
 
@@ -164,7 +166,15 @@ func (cgt *AwsCognitoProvider) CheckAuth(tokenString 
string) (*jwt.Token, errors
                return nil, fmt.Errorf("Public key not found")
        })
 
-       // Check if the token is invalid
+       if err != nil {
+               if ve, ok := err.(*jwt.ValidationError); ok {
+                       if ve.Errors == jwt.ValidationErrorExpired {
+                               return nil, errors.Forbidden.New("Token 
expired")
+                       }
+               }
+       }
+
+       // Check if the token is valid
        if err != nil || !token.Valid {
                cgt.logger.Error(err, "Invalid token")
                return nil, errors.Unauthorized.New("Invalid token")
@@ -175,11 +185,11 @@ func (cgt *AwsCognitoProvider) CheckAuth(tokenString 
string) (*jwt.Token, errors
                if actualClaims, ok := token.Claims.(jwt.MapClaims); ok {
                        for key, expected := range cgt.expectClaims {
                                if expected != actualClaims[key] {
-                                       return nil, 
errors.Unauthorized.New("Invalid token")
+                                       return nil, 
errors.Unauthorized.New("Invalid token: expected claims do not match")
                                }
                        }
                } else {
-                       return nil, errors.Unauthorized.New("Invalid token")
+                       return nil, errors.Unauthorized.New("Invalid token: 
expected claims do not match")
                }
        }
 
@@ -236,6 +246,18 @@ func (cgt *AwsCognitoProvider) NewPassword(newPasswordReq 
*NewPasswordRequest) (
        return loginRes, nil
 }
 
+func (cgt *AwsCognitoProvider) RefreshToken(req *RefreshTokenRequest) 
(*LoginResponse, errors.Error) {
+       // Create the input for InitiateAuth
+       input := &cognitoidentityprovider.InitiateAuthInput{
+               AuthFlow: 
aws.String(cognitoidentityprovider.AuthFlowTypeRefreshTokenAuth),
+               ClientId: cgt.clientId,
+               AuthParameters: map[string]*string{
+                       "REFRESH_TOKEN": aws.String(req.RefreshToken),
+               },
+       }
+       return cgt.initiateAuth(input)
+}
+
 // func (cgt *AwsCognitorProvider) ChangePassword(ctx *gin.Context, 
oldPassword, newPassword string) errors.Error {
 //     token := ctx.GetString(("token"))
 //     if token == "" {
diff --git a/config-ui/src/pages/login/login.tsx 
b/config-ui/src/pages/login/login.tsx
index d130797c8..69105f317 100644
--- a/config-ui/src/pages/login/login.tsx
+++ b/config-ui/src/pages/login/login.tsx
@@ -68,6 +68,7 @@ export const LoginPage = () => {
         setSession(res.session);
       } else {
         localStorage.setItem('accessToken', 
res.authenticationResult.accessToken);
+        localStorage.setItem('refreshToken', 
res.authenticationResult.refreshToken);
         document.cookie = 'access_token=' + 
res.authenticationResult.accessToken + '; path=/';
         setUsername('');
         setPassword('');
diff --git a/config-ui/src/utils/request.ts b/config-ui/src/utils/request.ts
index db676a21f..b345293e1 100644
--- a/config-ui/src/utils/request.ts
+++ b/config-ui/src/utils/request.ts
@@ -27,6 +27,8 @@ const instance = axios.create({
   baseURL: DEVLAKE_ENDPOINT,
 });
 
+var refreshingToken: Promise<any> | null = null;
+
 instance.interceptors.response.use(
   (response) => response,
   (error) => {
@@ -37,6 +39,39 @@ instance.interceptors.response.use(
       history.push('/login');
     }
 
+    if (status === 403) {
+      var refreshToken = localStorage.getItem('refreshToken');
+      if (refreshToken) {
+        refreshingToken =
+          refreshingToken ||
+          request('/login/refreshtoken', {
+            method: 'POST',
+            data: {
+              refreshToken: refreshToken,
+            },
+          }).then(
+            (resp) => {
+              localStorage.setItem('accessToken', 
resp.authenticationResult.accessToken);
+              refreshingToken = null;
+              return resp;
+            },
+            (err) => {
+              refreshingToken = null;
+              toast.error('Please login first');
+              history.push('/login');
+              return Promise.reject(err);
+            },
+          );
+        return refreshingToken.then(() => {
+          const originalRequest = error.config;
+          originalRequest._retry = true;
+          return Promise.resolve(request(originalRequest.url, 
originalRequest));
+        });
+      } else {
+        history.push('/login');
+      }
+    }
+
     if (status === 428) {
       history.push('/db-migrate');
     }

Reply via email to