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

zhangliang2022 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 109a89cbd Support NEW_PASSWORD_REQUIRED challenge (#5127)
109a89cbd is described below

commit 109a89cbd80d8e4e3b99bd8b6af31b9ad2a724a0
Author: Klesh Wong <[email protected]>
AuthorDate: Thu May 11 18:55:26 2023 +0800

    Support NEW_PASSWORD_REQUIRED challenge (#5127)
    
    * feat: support go only development
    
    * feat: BE supports NEW_PASSWORD_REQUIRED challenge
    
    * feat: config-ui supports NEW_PASSWORD_REQUIRED challenge
    
    * feat: support expect_claims to forbid unauthorized user
    
    * fix: typo and panic error when challengded
    
    * fix: no need to check error msg since test connection wont return 401 
anymore
    
    * fix: typo in swagger
    
    * fix: typo and panic error when challengded
---
 Makefile                                |   3 +
 backend/Makefile                        |   3 +
 backend/core/runner/loader.go           |   8 +-
 backend/server/api/api.go               |  12 +-
 backend/server/api/login/login.go       |  62 +++++----
 backend/server/services/auth/auth.go    | 108 ++++++++++++++++
 backend/server/services/auth/cognito.go | 215 +++++++++++++++++++++-----------
 backend/server/services/init.go         |   8 +-
 config-ui/src/pages/login/api.ts        |   8 ++
 config-ui/src/pages/login/login.tsx     |  58 +++++++--
 config-ui/src/utils/request.ts          |   4 +-
 env.example                             |   2 +
 12 files changed, 364 insertions(+), 127 deletions(-)

diff --git a/Makefile b/Makefile
index 2cb2222d6..1ad172eb8 100644
--- a/Makefile
+++ b/Makefile
@@ -96,6 +96,9 @@ worker:
 dev:
        make dev -C backend
 
+godev:
+       make godev -C backend
+
 debug:
        make debug -C backend
 
diff --git a/backend/Makefile b/backend/Makefile
index a13908eab..d07fb228d 100644
--- a/backend/Makefile
+++ b/backend/Makefile
@@ -70,6 +70,9 @@ worker:
 
 dev: build-plugin build-python run
 
+godev: 
+       DISABLED_REMOTE_PLUGINS=true make build-plugin run
+
 debug: build-plugin-debug
        dlv debug server/main.go
 
diff --git a/backend/core/runner/loader.go b/backend/core/runner/loader.go
index 327aafe7c..0690880c7 100644
--- a/backend/core/runner/loader.go
+++ b/backend/core/runner/loader.go
@@ -38,9 +38,11 @@ func LoadPlugins(basicRes context.BasicRes) errors.Error {
        if err != nil {
                return err
        }
-       err = LoadRemotePlugins(basicRes)
-       if err != nil {
-               return err
+       if !basicRes.GetConfigReader().GetBool("DISABLED_REMOTE_PLUGINS") {
+               err = LoadRemotePlugins(basicRes)
+               if err != nil {
+                       return err
+               }
        }
        return nil
 }
diff --git a/backend/server/api/api.go b/backend/server/api/api.go
index 390821d38..c9bf7d93e 100644
--- a/backend/server/api/api.go
+++ b/backend/server/api/api.go
@@ -57,27 +57,25 @@ Alternatively, you may downgrade back to the previous 
DevLake version.
 // @host localhost:8080
 // @BasePath /
 func CreateApiService() {
-       // Initialize services
-       services.Init()
        // Get configuration
        v := config.GetConfig()
+       // Initialize services
+       services.Init()
        // Set gin mode
        gin.SetMode(v.GetString("MODE"))
        // Create a gin router
        router := gin.Default()
 
-       // Check if AWS Cognito is enabled
-       awsCognitoEnabled := v.GetBool("AWS_ENABLE_COGNITO")
-
        // For both protected and unprotected routes
        router.GET("/ping", ping.Get)
        router.GET("/version", version.Get)
 
-       if awsCognitoEnabled {
+       if auth.Enabled() {
                // Add login endpoint
                router.POST("/login", login.Login)
+               router.POST("/login/newpassword", login.NewPassword)
                // Use AuthenticationMiddleware for protected routes
-               router.Use(auth.AuthenticationMiddleware)
+               router.Use(auth.Middleware)
        }
 
        // Endpoint to proceed database migration
diff --git a/backend/server/api/login/login.go 
b/backend/server/api/login/login.go
index 7655529c8..6b71ef41d 100644
--- a/backend/server/api/login/login.go
+++ b/backend/server/api/login/login.go
@@ -18,56 +18,66 @@ limitations under the License.
 package login
 
 import (
+       "net/http"
+
        "github.com/apache/incubator-devlake/core/errors"
        "github.com/apache/incubator-devlake/server/api/shared"
        "github.com/apache/incubator-devlake/server/services/auth"
-       "net/http"
 
        "github.com/gin-gonic/gin"
 )
 
-type LoginRequest struct {
-       Username string `json:"username"`
-       Password string `json:"password"`
-}
-
-type LoginResponse struct {
-       AuthenticationResult AuthenticationResult `json:"AuthenticationResult"`
-       ChallengeName        interface{}          `json:"ChallengeName"`
-       ChallengeParameters  ChallengeParameters  `json:"ChallengeParameters"`
-       Session              interface{}          `json:"Session"`
-}
-type AuthenticationResult struct {
-       AccessToken       string      `json:"AccessToken"`
-       ExpiresIn         int         `json:"ExpiresIn"`
-       IDToken           string      `json:"IdToken"`
-       NewDeviceMetadata interface{} `json:"NewDeviceMetadata"`
-       RefreshToken      string      `json:"RefreshToken"`
-       TokenType         string      `json:"TokenType"`
-}
-type ChallengeParameters struct {
-}
-
 // @Summary post login
 // @Description post login
 // @Tags framework/login
 // @Accept application/json
-// @Param blueprint body LoginRequest true "json"
+// @Param login body auth.LoginRequest true "json"
 // @Success 200  {object} LoginResponse
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
 // @Router /login [post]
 func Login(ctx *gin.Context) {
-       loginReq := &LoginRequest{}
+       loginReq := &auth.LoginRequest{}
        err := ctx.ShouldBind(loginReq)
        if err != nil {
                shared.ApiOutputError(ctx, errors.BadInput.Wrap(err, 
shared.BadRequestBody))
                return
        }
-       res, err := auth.SignIn(auth.CreateCognitoClient(), loginReq.Username, 
loginReq.Password)
+       res, err := auth.Provider.SignIn(loginReq)
        if err != nil {
                shared.ApiOutputError(ctx, errors.Default.Wrap(err, "error 
signing in"))
                return
        }
+       if res.AuthenticationResult != nil && 
res.AuthenticationResult.AccessToken != nil {
+               token, err := 
auth.Provider.CheckAuth(*res.AuthenticationResult.AccessToken)
+               if err != nil {
+                       shared.ApiOutputAbort(ctx, err)
+               }
+               ctx.Set("token", token)
+       }
+       shared.ApiOutputSuccess(ctx, res, http.StatusOK)
+}
+
+// @Summary post NewPassword
+// @Description post NewPassword
+// @Tags framework/NewPassword
+// @Accept application/json
+// @Param newpassword body auth.NewPasswordRequest true "json"
+// @Success 200  {object} shared.ApiBody
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /password [post]
+func NewPassword(ctx *gin.Context) {
+       newPasswordReq := &auth.NewPasswordRequest{}
+       err := ctx.ShouldBind(newPasswordReq)
+       if err != nil {
+               shared.ApiOutputError(ctx, errors.BadInput.Wrap(err, 
shared.BadRequestBody))
+               return
+       }
+       res, err := auth.Provider.NewPassword(newPasswordReq)
+       if err != nil {
+               shared.ApiOutputError(ctx, errors.BadInput.Wrap(err, "failed to 
set new password"))
+               return
+       }
        shared.ApiOutputSuccess(ctx, res, http.StatusOK)
 }
diff --git a/backend/server/services/auth/auth.go 
b/backend/server/services/auth/auth.go
new file mode 100644
index 000000000..1c1e78614
--- /dev/null
+++ b/backend/server/services/auth/auth.go
@@ -0,0 +1,108 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package auth
+
+import (
+       "strings"
+
+       "github.com/apache/incubator-devlake/core/context"
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/server/api/shared"
+       "github.com/dgrijalva/jwt-go"
+       "github.com/gin-gonic/gin"
+)
+
+// data structures
+
+type LoginRequest struct {
+       Username string `json:"username"`
+       Password string `json:"password"`
+}
+
+type LoginResponse struct {
+       AuthenticationResult *AuthenticationResult `json:"authenticationResult"`
+       ChallengeName        *string               `json:"challengeName"`
+       ChallengeParameters  map[string]*string    `json:"challengeParameters"`
+       Session              *string               `json:"session"`
+}
+
+type AuthenticationResult struct {
+       AccessToken  *string `json:"accessToken" type:"string" sensitive:"true"`
+       ExpiresIn    *int64  `json:"expiresIn" type:"integer"`
+       IdToken      *string `json:"idToken" type:"string" sensitive:"true"`
+       RefreshToken *string `json:"refreshToken" type:"string" 
sensitive:"true"`
+       TokenType    *string `json:"tokenType" type:"string"`
+}
+
+type ChallengeParameters struct {
+}
+
+type NewPasswordRequest struct {
+       Username    string `json:"username"`
+       NewPassword string `json:"newPassword"`
+       Session     string `json:"session"`
+}
+
+// auth provider interface
+type AuthProvider interface {
+       SignIn(*LoginRequest) (*LoginResponse, errors.Error)
+       NewPassword(*NewPasswordRequest) (*LoginResponse, errors.Error)
+       // ChangePassword(ctx *gin.Context, oldPassword, newPassword string) 
errors.Error
+       CheckAuth(token string) (*jwt.Token, errors.Error)
+}
+
+var Provider AuthProvider
+
+// initialize auth provider
+func InitProvider(basicRes context.BasicRes) {
+       v := basicRes.GetConfigReader()
+       awsCognitoEnabled := v.GetBool("AWS_ENABLE_COGNITO")
+       if awsCognitoEnabled {
+               Provider = NewCognitoProvider(basicRes)
+       }
+}
+
+func Middleware(ctx *gin.Context) {
+       if Provider == nil {
+               return
+       }
+       // Get the Auth header
+       authHeader := ctx.GetHeader("Authorization")
+       if authHeader == "" {
+               shared.ApiOutputAbort(ctx, 
errors.Unauthorized.New("Authorization header is missing"))
+               return
+       }
+
+       // Split the header into "Bearer" and the actual token
+       bearerToken := strings.Split(authHeader, " ")
+       if len(bearerToken) != 2 {
+               shared.ApiOutputAbort(ctx, errors.Unauthorized.New("Invalid 
Authorization header"))
+               return
+       }
+       token, err := Provider.CheckAuth(bearerToken[1])
+       if err != nil {
+               shared.ApiOutputAbort(ctx, err)
+               return
+       }
+
+       ctx.Set("token", token)
+}
+
+func Enabled() bool {
+       return Provider != nil
+}
diff --git a/backend/server/services/auth/cognito.go 
b/backend/server/services/auth/cognito.go
index d2872b13a..92a3748a4 100644
--- a/backend/server/services/auth/cognito.go
+++ b/backend/server/services/auth/cognito.go
@@ -26,129 +26,129 @@ import (
        "math/big"
        "net/http"
        "strings"
-       "sync"
 
        "github.com/apache/incubator-devlake/core/config"
-       "github.com/apache/incubator-devlake/impls/logruslog"
+       "github.com/apache/incubator-devlake/core/context"
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/log"
        "github.com/aws/aws-sdk-go/aws"
        "github.com/aws/aws-sdk-go/aws/session"
        "github.com/aws/aws-sdk-go/service/cognitoidentityprovider"
        "github.com/dgrijalva/jwt-go"
-       "github.com/gin-gonic/gin"
 )
 
-var (
-       //jwksCache is a cache of the fetched JWKS
-       jwksCache Jwks
-       //jwksCacheMx is a mutex to lock the jwksCache
-       jwksCacheMx sync.Mutex
-       logger      = logruslog.Global.Nested("auth")
-)
+type AwsCognitorProvider struct {
+       jwks         Jwks
+       logger       log.Logger
+       client       *cognitoidentityprovider.CognitoIdentityProvider
+       clientId     *string
+       expectClaims jwt.MapClaims
+}
 
-func CreateCognitoClient() *cognitoidentityprovider.CognitoIdentityProvider {
+func NewCognitoProvider(basicRes context.BasicRes) *AwsCognitorProvider {
        // Get configuration
        v := config.GetConfig()
+       // TODO: verify the configuration
        // Create an AWS session
        sess := session.Must(session.NewSession(&aws.Config{
                Region: aws.String(v.GetString("AWS_AUTH_REGION")),
        }))
        // Create a Cognito Identity Provider client
-       return cognitoidentityprovider.New(sess)
-}
-
-func SignIn(cognitoClient *cognitoidentityprovider.CognitoIdentityProvider, 
username, password string) (*cognitoidentityprovider.InitiateAuthOutput, error) 
{
-       // Get configuration
-       v := config.GetConfig()
-       // Create the input for InitiateAuth
-       input := &cognitoidentityprovider.InitiateAuthInput{
-               AuthFlow: aws.String("USER_PASSWORD_AUTH"),
-               ClientId: 
aws.String(v.GetString("AWS_AUTH_USER_POOL_WEB_CLIENT_ID")),
-               AuthParameters: map[string]*string{
-                       "USERNAME": aws.String(username),
-                       "PASSWORD": aws.String(password),
-               },
+       client := cognitoidentityprovider.New(sess)
+       cgt := &AwsCognitorProvider{
+               client:   client,
+               clientId: 
aws.String(v.GetString("AWS_AUTH_USER_POOL_WEB_CLIENT_ID")),
+               logger:   basicRes.GetLogger().Nested("cognito"),
        }
-
-       // Call Cognito to get auth tokens
-       response, err := cognitoClient.InitiateAuth(input)
+       // Fetch the JWKS from the Cognito User Pool
+       jwksURL := fmt.Sprintf(
+               "https://cognito-idp.%s.amazonaws.com/%s/.well-known/jwks.json";,
+               v.GetString("AWS_AUTH_REGION"),
+               v.GetString("AWS_AUTH_USER_POOL_ID"),
+       )
+       err := cgt.fetchJWKS(jwksURL)
        if err != nil {
-               return nil, err
+               panic(err)
+       }
+       // Optional expect claims
+       expect_claims := 
strings.TrimSpace(v.GetString("AWS_AUTH_EXPECT_CLAIMS"))
+       if expect_claims != "" {
+               e := json.Unmarshal([]byte(expect_claims), &cgt.expectClaims)
+               if e != nil {
+                       panic(e)
+               }
        }
-       return response, nil
+       return cgt
 }
 
-func fetchJWKS(jwksURL string) (jwks Jwks, err error) {
+func (cgt *AwsCognitorProvider) fetchJWKS(jwksURL string) errors.Error {
        // Get the JWKS from the URL
        resp, err := http.Get(jwksURL)
        if err != nil {
-               return
+               return errors.Default.Wrap(err, "Failed to fetch JWKS")
        }
        defer resp.Body.Close()
 
        body, err := io.ReadAll(resp.Body)
        if err != nil {
-               return
+               return errors.Default.Wrap(err, "Failed to read JWKS")
        }
        // Unmarshal the response into a Jwks struct
-       err = json.Unmarshal(body, &jwks)
-       return
+       err = json.Unmarshal(body, &cgt.jwks)
+       if err != nil {
+               return errors.Default.Wrap(err, "Failed to unmarshall JWKS")
+       }
+       return nil
 }
 
-func ensureJWKS(jwksURL string) (jwks Jwks, err error) {
-       // Lock the mutex
-       jwksCacheMx.Lock()
-       defer jwksCacheMx.Unlock()
-
-       // If the cache is empty, fetch the JWKS
-       if len(jwksCache.Keys) == 0 {
-               jwksCache, err = fetchJWKS(jwksURL)
+func (cgt *AwsCognitorProvider) SignIn(loginReq *LoginRequest) 
(*LoginResponse, errors.Error) {
+       // Create the input for InitiateAuth
+       input := &cognitoidentityprovider.InitiateAuthInput{
+               AuthFlow: aws.String("USER_PASSWORD_AUTH"),
+               ClientId: cgt.clientId,
+               AuthParameters: map[string]*string{
+                       "USERNAME": aws.String(loginReq.Username),
+                       "PASSWORD": aws.String(loginReq.Password),
+               },
        }
-       // Return the cached JWKS
-       jwks = jwksCache
-       return
-}
 
-func AuthenticationMiddleware(ctx *gin.Context) {
-       // Get configuration
-       v := config.GetConfig()
-       // Construct the JWKS URL
-       jwksURL := 
fmt.Sprintf("https://cognito-idp.%s.amazonaws.com/%s/.well-known/jwks.json";, 
v.GetString("AWS_AUTH_REGION"), v.GetString("AWS_AUTH_USER_POOL_ID"))
-       // Get the cached JWKS
-       jwks, err := ensureJWKS(jwksURL)
+       // Call Cognito to get auth tokens
+       response, err := cgt.client.InitiateAuth(input)
        if err != nil {
-               fmt.Printf("Error fetching JWKS: %v\n", err)
-               ctx.Abort()
-               return
+               return nil, errors.BadInput.New(err.Error())
        }
 
-       // Get the Auth header
-       authHeader := ctx.GetHeader("Authorization")
-       if authHeader == "" {
-               http.Error(ctx.Writer, "Authorization header is missing", 
http.StatusUnauthorized)
-               ctx.Abort()
-               return
+       loginRes := &LoginResponse{
+               ChallengeName:       response.ChallengeName,
+               ChallengeParameters: response.ChallengeParameters,
+               Session:             response.Session,
        }
-
-       // Split the header into "Bearer" and the actual token
-       bearerToken := strings.Split(authHeader, " ")
-       if len(bearerToken) != 2 {
-               http.Error(ctx.Writer, "Invalid Authorization header", 
http.StatusUnauthorized)
-               ctx.Abort()
-               return
+       if response.AuthenticationResult != nil {
+               loginRes.AuthenticationResult = &AuthenticationResult{
+                       AccessToken:  response.AuthenticationResult.AccessToken,
+                       ExpiresIn:    response.AuthenticationResult.ExpiresIn,
+                       IdToken:      response.AuthenticationResult.IdToken,
+                       RefreshToken: 
response.AuthenticationResult.RefreshToken,
+                       TokenType:    response.AuthenticationResult.TokenType,
+               }
        }
 
+       return loginRes, nil
+}
+
+func (cgt *AwsCognitorProvider) CheckAuth(tokenString string) (*jwt.Token, 
errors.Error) {
        // Parse the JWT token
-       token, err := jwt.Parse(bearerToken[1], func(token *jwt.Token) 
(interface{}, error) {
+       token, err := jwt.Parse(tokenString, func(token *jwt.Token) 
(interface{}, error) {
                // Check the signing method
                if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
-                       return nil, fmt.Errorf("Unexpected signing method: %v", 
token.Header["alg"])
+                       return nil, 
errors.Unauthorized.New(fmt.Sprintf("Unexpected signing method: %v", 
token.Header["alg"]))
                }
 
                // Get the key ID from the header
                kid := token.Header["kid"].(string)
 
                // Look for the key that matches the kid
-               for _, key := range jwks.Keys {
+               for _, key := range cgt.jwks.Keys {
                        if key.Kid == kid {
                                // Construct the RSA public key
                                n := pemHeader(key.N)
@@ -166,10 +166,24 @@ func AuthenticationMiddleware(ctx *gin.Context) {
 
        // Check if the token is invalid
        if err != nil || !token.Valid {
-               logger.Error(err, "Invalid token")
-               http.Error(ctx.Writer, "Invalid token", http.StatusUnauthorized)
-               ctx.Abort()
+               cgt.logger.Error(err, "Invalid token")
+               return nil, errors.Unauthorized.New("Invalid token")
+       }
+
+       // verify claims
+       if len(cgt.expectClaims) > 0 {
+               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")
+                               }
+                       }
+               } else {
+                       return nil, errors.Unauthorized.New("Invalid token")
+               }
        }
+
+       return token, nil
 }
 
 func pemHeader(encodedKey string) []byte {
@@ -189,3 +203,52 @@ type Jwks struct {
                E   string `json:"e"`
        } `json:"keys"`
 }
+
+func (cgt *AwsCognitorProvider) NewPassword(newPasswordReq 
*NewPasswordRequest) (*LoginResponse, errors.Error) {
+       input := &cognitoidentityprovider.RespondToAuthChallengeInput{
+               ChallengeName: aws.String("NEW_PASSWORD_REQUIRED"),
+               ChallengeResponses: map[string]*string{
+                       "USERNAME":     aws.String(newPasswordReq.Username),
+                       "NEW_PASSWORD": aws.String(newPasswordReq.NewPassword),
+               },
+               Session:  aws.String(newPasswordReq.Session),
+               ClientId: cgt.clientId,
+       }
+       response, err := cgt.client.RespondToAuthChallenge(input)
+       if err != nil {
+               return nil, errors.BadInput.Wrap(err, "Error setting up new 
password: "+err.Error())
+       }
+       // yes , it is identical to the login response, and yet they are 2 
different structs
+       loginRes := &LoginResponse{
+               ChallengeName:       response.ChallengeName,
+               ChallengeParameters: response.ChallengeParameters,
+               Session:             response.Session,
+       }
+       if response.AuthenticationResult != nil {
+               loginRes.AuthenticationResult = &AuthenticationResult{
+                       AccessToken:  response.AuthenticationResult.AccessToken,
+                       ExpiresIn:    response.AuthenticationResult.ExpiresIn,
+                       IdToken:      response.AuthenticationResult.IdToken,
+                       RefreshToken: 
response.AuthenticationResult.RefreshToken,
+                       TokenType:    response.AuthenticationResult.TokenType,
+               }
+       }
+       return loginRes, nil
+}
+
+// func (cgt *AwsCognitorProvider) ChangePassword(ctx *gin.Context, 
oldPassword, newPassword string) errors.Error {
+//     token := ctx.GetString(("token"))
+//     if token == "" {
+//             return errors.Unauthorized.New("Token is missing")
+//     }
+//     input := &cognitoidentityprovider.ChangePasswordInput{
+//             AccessToken:      &token,
+//             PreviousPassword: &oldPassword,
+//             ProposedPassword: &newPassword,
+//     }
+//     _, err := cgt.client.ChangePassword(input)
+//     if err != nil {
+//             return errors.BadInput.Wrap(err, "Error changing password")
+//     }
+//     return nil
+// }
diff --git a/backend/server/services/init.go b/backend/server/services/init.go
index 824a555c5..d1eead9fa 100644
--- a/backend/server/services/init.go
+++ b/backend/server/services/init.go
@@ -18,6 +18,9 @@ limitations under the License.
 package services
 
 import (
+       "sync"
+       "time"
+
        "github.com/apache/incubator-devlake/core/config"
        "github.com/apache/incubator-devlake/core/context"
        "github.com/apache/incubator-devlake/core/dal"
@@ -28,10 +31,9 @@ import (
        "github.com/apache/incubator-devlake/core/runner"
        "github.com/apache/incubator-devlake/impls/dalgorm"
        "github.com/apache/incubator-devlake/impls/logruslog"
+       "github.com/apache/incubator-devlake/server/services/auth"
        "github.com/go-playground/validator/v10"
        "github.com/robfig/cron/v3"
-       "sync"
-       "time"
 )
 
 var cfg config.ConfigReader
@@ -79,6 +81,8 @@ func GetMigrator() plugin.Migrator {
 func Init() {
        InitResources()
 
+       auth.InitProvider(basicRes)
+
        // lock the database to avoid multiple devlake instances from sharing 
the same one
        lockDb()
 
diff --git a/config-ui/src/pages/login/api.ts b/config-ui/src/pages/login/api.ts
index c5a749309..1e36a0a46 100644
--- a/config-ui/src/pages/login/api.ts
+++ b/config-ui/src/pages/login/api.ts
@@ -23,4 +23,12 @@ type LoginPayload = {
   password: string;
 };
 
+type NewPasswordPayload = {
+  username: string;
+  newPassword: string;
+  session: string;
+};
+
 export const login = (payload: LoginPayload) => request(`/login`, { method: 
'post', data: payload });
+export const newPassword = (payload: NewPasswordPayload) =>
+  request(`/login/newpassword`, { method: 'post', data: payload });
diff --git a/config-ui/src/pages/login/login.tsx 
b/config-ui/src/pages/login/login.tsx
index a45cc7f0e..fe3dba5c1 100644
--- a/config-ui/src/pages/login/login.tsx
+++ b/config-ui/src/pages/login/login.tsx
@@ -25,25 +25,51 @@ import { operator } from '@/utils';
 import * as API from './api';
 import * as S from './styld';
 
+const NEW_PASSWORD_REQUIRED = 'NEW_PASSWORD_REQUIRED';
+
 export const LoginPage = () => {
-  const [username, setUsername] = useState('');
+  const [username, setUsername] = useState(localStorage.getItem('username') || 
'');
   const [password, setPassword] = useState('');
+  const [newPassword, setNewPassword] = useState('');
+  const [challenge, setChallenge] = useState('');
+  const [session, setSession] = useState('');
 
   const history = useHistory();
 
+  // () =>
   const handleSubmit = async () => {
-    const [success, res] = await operator(() => API.login({ username, password 
}), {
-      formatReason: (error) => 'Login failed',
-    });
+    var request: () => Promise<any>;
 
-    if (success) {
-      localStorage.setItem('accessToken', 
res.AuthenticationResult.AccessToken);
-      document.cookie = 'access_token=' + res.AuthenticationResult.AccessToken 
+ '; path=/';
-      history.push('/');
+    switch (challenge) {
+      case NEW_PASSWORD_REQUIRED:
+        request = () => API.newPassword({ username, session, newPassword });
+        break;
+      default:
+        request = () => API.login({ username, password });
+        break;
     }
 
-    setUsername('');
-    setPassword('');
+    const [success, res] = await operator(request, {
+      formatReason: (error) => {
+        const e = error as any;
+        return e?.response?.data?.causes[0];
+      },
+    });
+    localStorage.setItem('username', username);
+    if (success) {
+      if (res.challengeName) {
+        setChallenge(res.challengeName);
+        setSession(res.session);
+      } else {
+        localStorage.setItem('accessToken', 
res.authenticationResult.accessToken);
+        document.cookie = 'access_token=' + 
res.authenticationResult.accessToken + '; path=/';
+        setUsername('');
+        setPassword('');
+        setChallenge('');
+        setSession('');
+        history.push('/');
+      }
+    }
   };
 
   return (
@@ -54,6 +80,7 @@ export const LoginPage = () => {
           <InputGroup
             placeholder="Username"
             value={username}
+            disabled={challenge !== ''}
             onChange={(e) => setUsername((e.target as HTMLInputElement).value)}
           />
         </FormGroup>
@@ -62,9 +89,20 @@ export const LoginPage = () => {
             type="password"
             placeholder="Password"
             value={password}
+            disabled={challenge !== ''}
             onChange={(e) => setPassword((e.target as HTMLInputElement).value)}
           />
         </FormGroup>
+        {challenge === 'NEW_PASSWORD_REQUIRED' && (
+          <FormGroup label="Set New Password">
+            <InputGroup
+              type="password"
+              placeholder="Please set a new Password for your account"
+              value={newPassword}
+              onChange={(e) => setNewPassword((e.target as 
HTMLInputElement).value)}
+            />
+          </FormGroup>
+        )}
         <Button intent={Intent.PRIMARY} onClick={handleSubmit}>
           Login
         </Button>
diff --git a/config-ui/src/utils/request.ts b/config-ui/src/utils/request.ts
index 388236481..db676a21f 100644
--- a/config-ui/src/utils/request.ts
+++ b/config-ui/src/utils/request.ts
@@ -27,14 +27,12 @@ const instance = axios.create({
   baseURL: DEVLAKE_ENDPOINT,
 });
 
-const Errors = ['Authorization header is missing', 'Invalid token'];
-
 instance.interceptors.response.use(
   (response) => response,
   (error) => {
     const status = error.response?.status;
 
-    if (status === 401 && Errors.some((err) => 
error.response.data.includes(err))) {
+    if (status === 401) {
       toast.error('Please login first');
       history.push('/login');
     }
diff --git a/env.example b/env.example
index 8c043663a..6c419f41b 100644
--- a/env.example
+++ b/env.example
@@ -35,6 +35,8 @@ FORCE_MIGRATION=false
 # Lake TAP API
 TAP_PROPERTIES_DIR=
 
+DISABLED_REMOTE_PLUGINS=
+
 ##########################
 # Sensitive information encryption key
 ##########################

Reply via email to