You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@devlake.apache.org by zh...@apache.org on 2023/05/11 10:55:31 UTC

[incubator-devlake] branch main updated: Support NEW_PASSWORD_REQUIRED challenge (#5127)

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 <zh...@merico.dev>
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
 ##########################