You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@devlake.apache.org by li...@apache.org on 2023/05/15 08:19:58 UTC

[incubator-devlake] branch main updated: Refresh token (#5174)

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 <zh...@merico.dev>
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');
     }