feat: support OIDC device flow: "/api/device-auth" (#3757)

This commit is contained in:
DacongDA 2025-04-30 23:42:26 +08:00 committed by GitHub
parent 36f5de3203
commit 383bf44391
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 252 additions and 4 deletions

View File

@ -47,6 +47,7 @@ p, *, *, GET, /api/get-app-login, *, *
p, *, *, POST, /api/logout, *, * p, *, *, POST, /api/logout, *, *
p, *, *, GET, /api/logout, *, * p, *, *, GET, /api/logout, *, *
p, *, *, POST, /api/callback, *, * p, *, *, POST, /api/callback, *, *
p, *, *, POST, /api/device-auth, *, *
p, *, *, GET, /api/get-account, *, * p, *, *, GET, /api/get-account, *, *
p, *, *, GET, /api/userinfo, *, * p, *, *, GET, /api/userinfo, *, *
p, *, *, GET, /api/user, *, * p, *, *, GET, /api/user, *, *

View File

@ -32,6 +32,7 @@ const (
ResponseTypeIdToken = "id_token" ResponseTypeIdToken = "id_token"
ResponseTypeSaml = "saml" ResponseTypeSaml = "saml"
ResponseTypeCas = "cas" ResponseTypeCas = "cas"
ResponseTypeDevice = "device"
) )
type Response struct { type Response struct {

View File

@ -25,6 +25,7 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/casdoor/casdoor/captcha" "github.com/casdoor/casdoor/captcha"
"github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/conf"
@ -169,6 +170,32 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
resp.Data2 = user.NeedUpdatePassword resp.Data2 = user.NeedUpdatePassword
} }
} else if form.Type == ResponseTypeDevice {
authCache, ok := object.DeviceAuthMap.LoadAndDelete(form.UserCode)
if !ok {
c.ResponseError(c.T("auth:UserCode Expired"))
return
}
authCacheCast := authCache.(object.DeviceAuthCache)
if authCacheCast.RequestAt.Add(time.Second * 120).Before(time.Now()) {
c.ResponseError(c.T("auth:UserCode Expired"))
return
}
deviceAuthCacheDeviceCode, ok := object.DeviceAuthMap.Load(authCacheCast.UserName)
if !ok {
c.ResponseError(c.T("auth:DeviceCode Invalid"))
return
}
deviceAuthCacheDeviceCodeCast := deviceAuthCacheDeviceCode.(object.DeviceAuthCache)
deviceAuthCacheDeviceCodeCast.UserName = user.Name
deviceAuthCacheDeviceCodeCast.UserSignIn = true
object.DeviceAuthMap.Store(authCacheCast.UserName, deviceAuthCacheDeviceCodeCast)
resp = &Response{Status: "ok", Msg: "", Data: userId, Data2: user.NeedUpdatePassword}
} else if form.Type == ResponseTypeSaml { // saml flow } else if form.Type == ResponseTypeSaml { // saml flow
res, redirectUrl, method, err := object.GetSamlResponse(application, user, form.SamlRequest, c.Ctx.Request.Host) res, redirectUrl, method, err := object.GetSamlResponse(application, user, form.SamlRequest, c.Ctx.Request.Host)
if err != nil { if err != nil {
@ -242,6 +269,7 @@ func (c *ApiController) GetApplicationLogin() {
state := c.Input().Get("state") state := c.Input().Get("state")
id := c.Input().Get("id") id := c.Input().Get("id")
loginType := c.Input().Get("type") loginType := c.Input().Get("type")
userCode := c.Input().Get("userCode")
var application *object.Application var application *object.Application
var msg string var msg string
@ -268,6 +296,19 @@ func (c *ApiController) GetApplicationLogin() {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
} else if loginType == "device" {
deviceAuthCache, ok := object.DeviceAuthMap.Load(userCode)
if !ok {
c.ResponseError(c.T("auth:UserCode Invalid"))
return
}
deviceAuthCacheCast := deviceAuthCache.(object.DeviceAuthCache)
application, err = object.GetApplication(deviceAuthCacheCast.ApplicationId)
if err != nil {
c.ResponseError(err.Error())
return
}
} }
clientIp := util.GetClientIpFromRequest(c.Ctx.Request) clientIp := util.GetClientIpFromRequest(c.Ctx.Request)
@ -1215,3 +1256,75 @@ func (c *ApiController) Callback() {
frontendCallbackUrl := fmt.Sprintf("/callback?code=%s&state=%s", code, state) frontendCallbackUrl := fmt.Sprintf("/callback?code=%s&state=%s", code, state)
c.Ctx.Redirect(http.StatusFound, frontendCallbackUrl) c.Ctx.Redirect(http.StatusFound, frontendCallbackUrl)
} }
// DeviceAuth
// @Title DeviceAuth
// @Tag Device Authorization Endpoint
// @Description Endpoint for the device authorization flow
// @router /device-auth [post]
// @Success 200 {object} object.DeviceAuthResponse The Response object
func (c *ApiController) DeviceAuth() {
clientId := c.Input().Get("client_id")
scope := c.Input().Get("scope")
application, err := object.GetApplicationByClientId(clientId)
if err != nil {
c.Data["json"] = object.TokenError{
Error: err.Error(),
ErrorDescription: err.Error(),
}
c.ServeJSON()
return
}
if application == nil {
c.Data["json"] = object.TokenError{
Error: c.T("token:Invalid client_id"),
ErrorDescription: c.T("token:Invalid client_id"),
}
c.ServeJSON()
return
}
deviceCode := util.GenerateId()
userCode := util.GetRandomName()
generateTime := 0
for {
if generateTime > 5 {
c.Data["json"] = object.TokenError{
Error: "userCode gen",
ErrorDescription: c.T("token:Invalid client_id"),
}
c.ServeJSON()
return
}
_, ok := object.DeviceAuthMap.Load(userCode)
if !ok {
break
}
generateTime++
}
deviceAuthCache := object.DeviceAuthCache{
UserSignIn: false,
UserName: "",
Scope: scope,
ApplicationId: application.GetId(),
RequestAt: time.Now(),
}
userAuthCache := object.DeviceAuthCache{
UserSignIn: false,
UserName: deviceCode,
Scope: scope,
ApplicationId: application.GetId(),
RequestAt: time.Now(),
}
object.DeviceAuthMap.Store(deviceCode, deviceAuthCache)
object.DeviceAuthMap.Store(userCode, userAuthCache)
c.Data["json"] = object.GetDeviceAuthResponse(deviceCode, userCode, c.Ctx.Request.Host)
c.ServeJSON()
}

View File

@ -16,6 +16,7 @@ package controllers
import ( import (
"encoding/json" "encoding/json"
"time"
"github.com/beego/beego/utils/pagination" "github.com/beego/beego/utils/pagination"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
@ -170,12 +171,17 @@ func (c *ApiController) GetOAuthToken() {
tag := c.Input().Get("tag") tag := c.Input().Get("tag")
avatar := c.Input().Get("avatar") avatar := c.Input().Get("avatar")
refreshToken := c.Input().Get("refresh_token") refreshToken := c.Input().Get("refresh_token")
deviceCode := c.Input().Get("device_code")
if clientId == "" && clientSecret == "" { if clientId == "" && clientSecret == "" {
clientId, clientSecret, _ = c.Ctx.Request.BasicAuth() clientId, clientSecret, _ = c.Ctx.Request.BasicAuth()
} }
if len(c.Ctx.Input.RequestBody) != 0 { if grantType == "urn:ietf:params:oauth:grant-type:device_code" {
clientId, clientSecret, _ = c.Ctx.Request.BasicAuth()
}
if len(c.Ctx.Input.RequestBody) != 0 && grantType != "urn:ietf:params:oauth:grant-type:device_code" {
// If clientId is empty, try to read data from RequestBody // If clientId is empty, try to read data from RequestBody
var tokenRequest TokenRequest var tokenRequest TokenRequest
err := json.Unmarshal(c.Ctx.Input.RequestBody, &tokenRequest) err := json.Unmarshal(c.Ctx.Input.RequestBody, &tokenRequest)
@ -219,6 +225,40 @@ func (c *ApiController) GetOAuthToken() {
} }
} }
if deviceCode != "" {
deviceAuthCache, ok := object.DeviceAuthMap.Load(deviceCode)
if !ok {
c.Data["json"] = object.TokenError{
Error: "expired_token",
ErrorDescription: "token is expired",
}
c.ServeJSON()
return
}
deviceAuthCacheCast := deviceAuthCache.(object.DeviceAuthCache)
if !deviceAuthCacheCast.UserSignIn {
c.Data["json"] = object.TokenError{
Error: "authorization_pending",
ErrorDescription: "authorization pending",
}
c.ServeJSON()
return
}
if deviceAuthCacheCast.RequestAt.Add(time.Second * 120).Before(time.Now()) {
c.Data["json"] = object.TokenError{
Error: "expired_token",
ErrorDescription: "token is expired",
}
c.ServeJSON()
return
}
object.DeviceAuthMap.Delete(deviceCode)
username = deviceAuthCacheCast.UserName
}
host := c.Ctx.Request.Host host := c.Ctx.Request.Host
token, err := object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, nonce, username, password, host, refreshToken, tag, avatar, c.GetAcceptLanguage()) token, err := object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, nonce, username, password, host, refreshToken, tag, avatar, c.GetAcceptLanguage())
if err != nil { if err != nil {

View File

@ -70,6 +70,7 @@ type AuthForm struct {
FaceId []float64 `json:"faceId"` FaceId []float64 `json:"faceId"`
FaceIdImage []string `json:"faceIdImage"` FaceIdImage []string `json:"faceIdImage"`
UserCode string `json:"userCode"`
} }
func GetAuthFormFieldValue(form *AuthForm, fieldName string) (bool, string) { func GetAuthFormFieldValue(form *AuthForm, fieldName string) (bool, string) {

View File

@ -30,6 +30,7 @@ type OidcDiscovery struct {
AuthorizationEndpoint string `json:"authorization_endpoint"` AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"` TokenEndpoint string `json:"token_endpoint"`
UserinfoEndpoint string `json:"userinfo_endpoint"` UserinfoEndpoint string `json:"userinfo_endpoint"`
DeviceAuthorizationEndpoint string `json:"device_authorization_endpoint"`
JwksUri string `json:"jwks_uri"` JwksUri string `json:"jwks_uri"`
IntrospectionEndpoint string `json:"introspection_endpoint"` IntrospectionEndpoint string `json:"introspection_endpoint"`
ResponseTypesSupported []string `json:"response_types_supported"` ResponseTypesSupported []string `json:"response_types_supported"`
@ -119,6 +120,7 @@ func GetOidcDiscovery(host string) OidcDiscovery {
AuthorizationEndpoint: fmt.Sprintf("%s/login/oauth/authorize", originFrontend), AuthorizationEndpoint: fmt.Sprintf("%s/login/oauth/authorize", originFrontend),
TokenEndpoint: fmt.Sprintf("%s/api/login/oauth/access_token", originBackend), TokenEndpoint: fmt.Sprintf("%s/api/login/oauth/access_token", originBackend),
UserinfoEndpoint: fmt.Sprintf("%s/api/userinfo", originBackend), UserinfoEndpoint: fmt.Sprintf("%s/api/userinfo", originBackend),
DeviceAuthorizationEndpoint: fmt.Sprintf("%s/api/device-auth", originBackend),
JwksUri: fmt.Sprintf("%s/.well-known/jwks", originBackend), JwksUri: fmt.Sprintf("%s/.well-known/jwks", originBackend),
IntrospectionEndpoint: fmt.Sprintf("%s/api/login/oauth/introspect", originBackend), IntrospectionEndpoint: fmt.Sprintf("%s/api/login/oauth/introspect", originBackend),
ResponseTypesSupported: []string{"code", "token", "id_token", "code token", "code id_token", "token id_token", "code token id_token", "none"}, ResponseTypesSupported: []string{"code", "token", "id_token", "code token", "code id_token", "token id_token", "code token id_token", "none"},
@ -213,3 +215,14 @@ func GetWebFinger(resource string, rels []string, host string) (WebFinger, error
return wf, nil return wf, nil
} }
func GetDeviceAuthResponse(deviceCode string, userCode string, host string) DeviceAuthResponse {
originFrontend, _ := getOriginFromHost(host)
return DeviceAuthResponse{
DeviceCode: deviceCode,
UserCode: userCode,
VerificationUri: fmt.Sprintf("%s/login/oauth/device/%s", originFrontend, userCode),
ExpiresIn: 120,
}
}

View File

@ -18,6 +18,7 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"sync"
"time" "time"
"github.com/casdoor/casdoor/i18n" "github.com/casdoor/casdoor/i18n"
@ -37,6 +38,8 @@ const (
EndpointError = "endpoint_error" EndpointError = "endpoint_error"
) )
var DeviceAuthMap = sync.Map{}
type Code struct { type Code struct {
Message string `xorm:"varchar(100)" json:"message"` Message string `xorm:"varchar(100)" json:"message"`
Code string `xorm:"varchar(100)" json:"code"` Code string `xorm:"varchar(100)" json:"code"`
@ -71,6 +74,22 @@ type IntrospectionResponse struct {
Jti string `json:"jti,omitempty"` Jti string `json:"jti,omitempty"`
} }
type DeviceAuthCache struct {
UserSignIn bool
UserName string
ApplicationId string
Scope string
RequestAt time.Time
}
type DeviceAuthResponse struct {
DeviceCode string `json:"device_code"`
UserCode string `json:"user_code"`
VerificationUri string `json:"verification_uri"`
ExpiresIn int `json:"expires_in"`
Interval int `json:"interval"`
}
func ExpireTokenByAccessToken(accessToken string) (bool, *Application, *Token, error) { func ExpireTokenByAccessToken(accessToken string) (bool, *Application, *Token, error) {
token, err := GetTokenByAccessToken(accessToken) token, err := GetTokenByAccessToken(accessToken)
if err != nil { if err != nil {
@ -222,6 +241,8 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code
token, tokenError, err = GetClientCredentialsToken(application, clientSecret, scope, host) token, tokenError, err = GetClientCredentialsToken(application, clientSecret, scope, host)
case "token", "id_token": // Implicit Grant case "token", "id_token": // Implicit Grant
token, tokenError, err = GetImplicitToken(application, username, scope, nonce, host) token, tokenError, err = GetImplicitToken(application, username, scope, nonce, host)
case "urn:ietf:params:oauth:grant-type:device_code":
token, tokenError, err = GetImplicitToken(application, username, scope, nonce, host)
case "refresh_token": case "refresh_token":
refreshToken2, err := RefreshToken(grantType, refreshToken, scope, clientId, clientSecret, host) refreshToken2, err := RefreshToken(grantType, refreshToken, scope, clientId, clientSecret, host)
if err != nil { if err != nil {

View File

@ -66,6 +66,7 @@ func initAPI() {
beego.Router("/api/get-webhook-event", &controllers.ApiController{}, "GET:GetWebhookEventType") beego.Router("/api/get-webhook-event", &controllers.ApiController{}, "GET:GetWebhookEventType")
beego.Router("/api/get-captcha-status", &controllers.ApiController{}, "GET:GetCaptchaStatus") beego.Router("/api/get-captcha-status", &controllers.ApiController{}, "GET:GetCaptchaStatus")
beego.Router("/api/callback", &controllers.ApiController{}, "POST:Callback") beego.Router("/api/callback", &controllers.ApiController{}, "POST:Callback")
beego.Router("/api/device-auth", &controllers.ApiController{}, "POST:DeviceAuth")
beego.Router("/api/get-organizations", &controllers.ApiController{}, "GET:GetOrganizations") beego.Router("/api/get-organizations", &controllers.ApiController{}, "GET:GetOrganizations")
beego.Router("/api/get-organization", &controllers.ApiController{}, "GET:GetOrganization") beego.Router("/api/get-organization", &controllers.ApiController{}, "GET:GetOrganization")

View File

@ -726,6 +726,7 @@ class ApplicationEditPage extends React.Component {
{id: "token", name: "Token"}, {id: "token", name: "Token"},
{id: "id_token", name: "ID Token"}, {id: "id_token", name: "ID Token"},
{id: "refresh_token", name: "Refresh Token"}, {id: "refresh_token", name: "Refresh Token"},
{id: "urn:ietf:params:oauth:grant-type:device_code", name: "Device Code"},
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>) ].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
} }
</Select> </Select>

View File

@ -119,6 +119,7 @@ class EntryPage extends React.Component {
<Route exact path="/login/:owner" render={(props) => this.renderHomeIfLoggedIn(<SelfLoginPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} /> <Route exact path="/login/:owner" render={(props) => this.renderHomeIfLoggedIn(<SelfLoginPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />)} />
<Route exact path="/signup/oauth/authorize" render={(props) => <SignupPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />} /> <Route exact path="/signup/oauth/authorize" render={(props) => <SignupPage {...this.props} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />} />
<Route exact path="/login/oauth/authorize" render={(props) => <LoginPage {...this.props} application={this.state.application} type={"code"} mode={"signin"} onUpdateApplication={onUpdateApplication} {...props} />} /> <Route exact path="/login/oauth/authorize" render={(props) => <LoginPage {...this.props} application={this.state.application} type={"code"} mode={"signin"} onUpdateApplication={onUpdateApplication} {...props} />} />
<Route exact path="/login/oauth/device/:userCode" render={(props) => <LoginPage {...this.props} application={this.state.application} type={"device"} mode={"signin"} onUpdateApplication={onUpdateApplication} {...props} />} />
<Route exact path="/login/saml/authorize/:owner/:applicationName" render={(props) => <LoginPage {...this.props} application={this.state.application} type={"saml"} mode={"signin"} onUpdateApplication={onUpdateApplication} {...props} />} /> <Route exact path="/login/saml/authorize/:owner/:applicationName" render={(props) => <LoginPage {...this.props} application={this.state.application} type={"saml"} mode={"signin"} onUpdateApplication={onUpdateApplication} {...props} />} />
<Route exact path="/forget" render={(props) => <SelfForgetPage {...this.props} account={this.props.account} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />} /> <Route exact path="/forget" render={(props) => <SelfForgetPage {...this.props} account={this.props.account} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />} />
<Route exact path="/forget/:applicationName" render={(props) => <ForgetPage {...this.props} account={this.props.account} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />} /> <Route exact path="/forget/:applicationName" render={(props) => <ForgetPage {...this.props} account={this.props.account} application={this.state.application} onUpdateApplication={onUpdateApplication} {...props} />} />

View File

@ -61,7 +61,14 @@ export function oAuthParamsToQuery(oAuthParams) {
} }
export function getApplicationLogin(params) { export function getApplicationLogin(params) {
const queryParams = (params?.type === "cas") ? casLoginParamsToQuery(params) : oAuthParamsToQuery(params); let queryParams = "";
if (params?.type === "cas") {
queryParams = casLoginParamsToQuery(params);
} else if (params?.type === "device") {
queryParams = `?userCode=${params.userCode}&type=device`;
} else {
queryParams = oAuthParamsToQuery(params);
}
return fetch(`${authConfig.serverUrl}/api/get-app-login${queryParams}`, { return fetch(`${authConfig.serverUrl}/api/get-app-login${queryParams}`, {
method: "GET", method: "GET",
credentials: "include", credentials: "include",

View File

@ -65,6 +65,8 @@ class LoginPage extends React.Component {
orgChoiceMode: new URLSearchParams(props.location?.search).get("orgChoiceMode") ?? null, orgChoiceMode: new URLSearchParams(props.location?.search).get("orgChoiceMode") ?? null,
userLang: null, userLang: null,
loginLoading: false, loginLoading: false,
userCode: props.userCode ?? (props.match?.params?.userCode ?? null),
userCodeStatus: "",
}; };
if (this.state.type === "cas" && props.match?.params.casApplicationName !== undefined) { if (this.state.type === "cas" && props.match?.params.casApplicationName !== undefined) {
@ -81,7 +83,7 @@ class LoginPage extends React.Component {
if (this.getApplicationObj() === undefined) { if (this.getApplicationObj() === undefined) {
if (this.state.type === "login" || this.state.type === "saml") { if (this.state.type === "login" || this.state.type === "saml") {
this.getApplication(); this.getApplication();
} else if (this.state.type === "code" || this.state.type === "cas") { } else if (this.state.type === "code" || this.state.type === "cas" || this.state.type === "device") {
this.getApplicationLogin(); this.getApplicationLogin();
} else { } else {
Setting.showMessage("error", `Unknown authentication type: ${this.state.type}`); Setting.showMessage("error", `Unknown authentication type: ${this.state.type}`);
@ -155,13 +157,25 @@ class LoginPage extends React.Component {
} }
getApplicationLogin() { getApplicationLogin() {
const loginParams = (this.state.type === "cas") ? Util.getCasLoginParameters("admin", this.state.applicationName) : Util.getOAuthGetParameters(); let loginParams;
if (this.state.type === "cas") {
loginParams = Util.getCasLoginParameters("admin", this.state.applicationName);
} else if (this.state.type === "device") {
loginParams = {userCode: this.state.userCode, type: this.state.type};
} else {
loginParams = Util.getOAuthGetParameters();
}
AuthBackend.getApplicationLogin(loginParams) AuthBackend.getApplicationLogin(loginParams)
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
const application = res.data; const application = res.data;
this.onUpdateApplication(application); this.onUpdateApplication(application);
} else { } else {
if (this.state.type === "device") {
this.setState({
userCodeStatus: "expired",
});
}
this.onUpdateApplication(null); this.onUpdateApplication(null);
this.setState({ this.setState({
msg: res.msg, msg: res.msg,
@ -266,6 +280,9 @@ class LoginPage extends React.Component {
onUpdateApplication(application) { onUpdateApplication(application) {
this.props.onUpdateApplication(application); this.props.onUpdateApplication(application);
if (application === null) {
return;
}
for (const idx in application.providers) { for (const idx in application.providers) {
const provider = application.providers[idx]; const provider = application.providers[idx];
if (provider.provider?.category === "Face ID") { if (provider.provider?.category === "Face ID") {
@ -296,6 +313,9 @@ class LoginPage extends React.Component {
const oAuthParams = Util.getOAuthGetParameters(); const oAuthParams = Util.getOAuthGetParameters();
values["type"] = oAuthParams?.responseType ?? this.state.type; values["type"] = oAuthParams?.responseType ?? this.state.type;
if (this.state.userCode) {
values["userCode"] = this.state.userCode;
}
if (oAuthParams?.samlRequest) { if (oAuthParams?.samlRequest) {
values["samlRequest"] = oAuthParams.samlRequest; values["samlRequest"] = oAuthParams.samlRequest;
@ -479,6 +499,11 @@ class LoginPage extends React.Component {
this.props.onLoginSuccess(); this.props.onLoginSuccess();
} else if (responseType === "code") { } else if (responseType === "code") {
this.postCodeLoginAction(res); this.postCodeLoginAction(res);
} else if (responseType === "device") {
Setting.showMessage("success", "Successful login");
this.setState({
userCodeStatus: "success",
});
} else if (responseType === "token" || responseType === "id_token") { } else if (responseType === "token" || responseType === "id_token") {
if (res.data2) { if (res.data2) {
sessionStorage.setItem("signinUrl", window.location.pathname + window.location.search); sessionStorage.setItem("signinUrl", window.location.pathname + window.location.search);
@ -826,6 +851,16 @@ class LoginPage extends React.Component {
); );
} }
if (this.state.userCode && this.state.userCodeStatus === "success") {
return (
<Result
status="success"
title={i18next.t("application:Logged in successfully")}
>
</Result>
);
}
const showForm = Setting.isPasswordEnabled(application) || Setting.isCodeSigninEnabled(application) || Setting.isWebAuthnEnabled(application) || Setting.isLdapEnabled(application) || Setting.isFaceIdEnabled(application); const showForm = Setting.isPasswordEnabled(application) || Setting.isCodeSigninEnabled(application) || Setting.isWebAuthnEnabled(application) || Setting.isLdapEnabled(application) || Setting.isFaceIdEnabled(application);
if (showForm) { if (showForm) {
let loginWidth = 320; let loginWidth = 320;
@ -986,6 +1021,10 @@ class LoginPage extends React.Component {
return null; return null;
} }
if (this.state.userCode && this.state.userCodeStatus === "success") {
return null;
}
return ( return (
<div> <div>
<div style={{fontSize: 16, textAlign: "left"}}> <div style={{fontSize: 16, textAlign: "left"}}>
@ -1268,6 +1307,15 @@ class LoginPage extends React.Component {
} }
render() { render() {
if (this.state.userCodeStatus === "expired") {
return <Result
style={{width: "100%"}}
status="error"
title={`Code ${i18next.t("subscription:Expired")}`}
>
</Result>;
}
const application = this.getApplicationObj(); const application = this.getApplicationObj();
if (application === undefined) { if (application === undefined) {
return null; return null;