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
##########################