mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-03 12:30:19 +08:00
feat: support webauthn (#407)
* feat: support webauthn * Update init.go * Update user_webauthn.go * Update UserEditPage.js * Update WebauthnCredentialTable.js * Update LoginPage.js Co-authored-by: Gucheng <85475922+nomeguy@users.noreply.github.com>
This commit is contained in:

committed by
GitHub

parent
208dc11d25
commit
7f3b2500b3
@ -105,6 +105,7 @@ p, *, *, GET, /api/get-saml-login, *, *
|
|||||||
p, *, *, POST, /api/acs, *, *
|
p, *, *, POST, /api/acs, *, *
|
||||||
p, *, *, GET, /api/saml/metadata, *, *
|
p, *, *, GET, /api/saml/metadata, *, *
|
||||||
p, *, *, *, /cas, *, *
|
p, *, *, *, /cas, *, *
|
||||||
|
p, *, *, *, /api/webauthn, *, *
|
||||||
`
|
`
|
||||||
|
|
||||||
sa := stringadapter.NewAdapter(ruleText)
|
sa := stringadapter.NewAdapter(ruleText)
|
||||||
|
138
controllers/webauthn.go
Normal file
138
controllers/webauthn.go
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed 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 controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"github.com/casdoor/casdoor/object"
|
||||||
|
"github.com/casdoor/casdoor/util"
|
||||||
|
"github.com/duo-labs/webauthn/protocol"
|
||||||
|
"github.com/duo-labs/webauthn/webauthn"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @Title WebAuthnSignupBegin
|
||||||
|
// @Tag User API
|
||||||
|
// @Description WebAuthn Registration Flow 1st stage
|
||||||
|
// @Success 200 {object} protocol.CredentialCreation The CredentialCreationOptions object
|
||||||
|
// @router /webauthn/signup/begin [get]
|
||||||
|
func (c *ApiController) WebAuthnSignupBegin() {
|
||||||
|
webauthnObj := object.GetWebAuthnObject(c.Ctx.Request.Host)
|
||||||
|
user := c.getCurrentUser()
|
||||||
|
if user == nil {
|
||||||
|
c.ResponseError("Please login first.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
registerOptions := func(credCreationOpts *protocol.PublicKeyCredentialCreationOptions) {
|
||||||
|
credCreationOpts.CredentialExcludeList = user.CredentialExcludeList()
|
||||||
|
}
|
||||||
|
options, sessionData, err := webauthnObj.BeginRegistration(
|
||||||
|
user,
|
||||||
|
registerOptions,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.SetSession("registration", *sessionData)
|
||||||
|
c.Data["json"] = options
|
||||||
|
c.ServeJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Title WebAuthnSignupFinish
|
||||||
|
// @Tag User API
|
||||||
|
// @Description WebAuthn Registration Flow 2nd stage
|
||||||
|
// @Param body body protocol.CredentialCreationResponse true "authenticator attestation Response"
|
||||||
|
// @Success 200 {object} Response "The Response object"
|
||||||
|
// @router /webauthn/signup/finish [post]
|
||||||
|
func (c *ApiController) WebAuthnSignupFinish() {
|
||||||
|
webauthnObj := object.GetWebAuthnObject(c.Ctx.Request.Host)
|
||||||
|
user := c.getCurrentUser()
|
||||||
|
if user == nil {
|
||||||
|
c.ResponseError("Please login first.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sessionObj := c.GetSession("registration")
|
||||||
|
sessionData, ok := sessionObj.(webauthn.SessionData)
|
||||||
|
if !ok {
|
||||||
|
c.ResponseError("Please call WebAuthnSignupBegin first")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer(c.Ctx.Input.RequestBody))
|
||||||
|
|
||||||
|
credential, err := webauthnObj.FinishRegistration(user, sessionData, c.Ctx.Request)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isGlobalAdmin := c.IsGlobalAdmin()
|
||||||
|
user.AddCredentials(*credential, isGlobalAdmin)
|
||||||
|
c.ResponseOk()
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Title WebAuthnSigninBegin
|
||||||
|
// @Tag Login API
|
||||||
|
// @Description WebAuthn Login Flow 1st stage
|
||||||
|
// @Param owner query string true "owner"
|
||||||
|
// @Param name query string true "name"
|
||||||
|
// @Success 200 {object} protocol.CredentialAssertion The CredentialAssertion object
|
||||||
|
// @router /webauthn/signin/begin [get]
|
||||||
|
func (c *ApiController) WebAuthnSigninBegin() {
|
||||||
|
webauthnObj := object.GetWebAuthnObject(c.Ctx.Request.Host)
|
||||||
|
userOwner := c.Input().Get("owner")
|
||||||
|
userName := c.Input().Get("name")
|
||||||
|
user := object.GetUserByFields(userOwner, userName)
|
||||||
|
if user == nil {
|
||||||
|
c.ResponseError("Please Giveout Owner and Username.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
options, sessionData, err := webauthnObj.BeginLogin(user)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.SetSession("authentication", *sessionData)
|
||||||
|
c.Data["json"] = options
|
||||||
|
c.ServeJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Title WebAuthnSigninBegin
|
||||||
|
// @Tag Login API
|
||||||
|
// @Description WebAuthn Login Flow 2nd stage
|
||||||
|
// @Param body body protocol.CredentialAssertionResponse true "authenticator assertion Response"
|
||||||
|
// @Success 200 {object} Response "The Response object"
|
||||||
|
// @router /webauthn/signin/finish [post]
|
||||||
|
func (c *ApiController) WebAuthnSigninFinish() {
|
||||||
|
webauthnObj := object.GetWebAuthnObject(c.Ctx.Request.Host)
|
||||||
|
sessionObj := c.GetSession("authentication")
|
||||||
|
sessionData, ok := sessionObj.(webauthn.SessionData)
|
||||||
|
if !ok {
|
||||||
|
c.ResponseError("Please call WebAuthnSigninBegin first")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer(c.Ctx.Input.RequestBody))
|
||||||
|
userId := string(sessionData.UserID)
|
||||||
|
user := object.GetUser(userId)
|
||||||
|
_, err := webauthnObj.FinishLogin(user, sessionData, c.Ctx.Request)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.SetSessionUsername(userId)
|
||||||
|
util.LogInfo(c.Ctx, "API: [%s] signed in", userId)
|
||||||
|
c.ResponseOk(userId)
|
||||||
|
}
|
1
go.mod
1
go.mod
@ -14,6 +14,7 @@ require (
|
|||||||
github.com/casdoor/goth v1.69.0-FIX2
|
github.com/casdoor/goth v1.69.0-FIX2
|
||||||
github.com/casdoor/oss v1.2.0
|
github.com/casdoor/oss v1.2.0
|
||||||
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
|
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
|
||||||
|
github.com/duo-labs/webauthn v0.0.0-20211221191814-a22482edaa3b
|
||||||
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df
|
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df
|
||||||
github.com/go-ldap/ldap/v3 v3.3.0
|
github.com/go-ldap/ldap/v3 v3.3.0
|
||||||
github.com/go-pay/gopay v1.5.72
|
github.com/go-pay/gopay v1.5.72
|
||||||
|
14
go.sum
14
go.sum
@ -111,6 +111,8 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR
|
|||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/cloudflare/cfssl v0.0.0-20190726000631-633726f6bcb7 h1:Puu1hUwfps3+1CUzYdAZXijuvLuRMirgiXdf3zsM2Ig=
|
||||||
|
github.com/cloudflare/cfssl v0.0.0-20190726000631-633726f6bcb7/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
|
||||||
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
|
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
|
||||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
github.com/couchbase/go-couchbase v0.0.0-20200519150804-63f3cdb75e0d/go.mod h1:TWI8EKQMs5u5jLKW/tsb9VwauIrMIxQG1r5fMsswK5U=
|
github.com/couchbase/go-couchbase v0.0.0-20200519150804-63f3cdb75e0d/go.mod h1:TWI8EKQMs5u5jLKW/tsb9VwauIrMIxQG1r5fMsswK5U=
|
||||||
@ -124,6 +126,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
|||||||
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f h1:q/DpyjJjZs94bziQ7YkBmIlpqbVP7yw179rnzoNVX1M=
|
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f h1:q/DpyjJjZs94bziQ7YkBmIlpqbVP7yw179rnzoNVX1M=
|
||||||
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f/go.mod h1:QGrK8vMWWHQYQ3QU9bw9Y9OPNfxccGzfb41qjvVeXtY=
|
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f/go.mod h1:QGrK8vMWWHQYQ3QU9bw9Y9OPNfxccGzfb41qjvVeXtY=
|
||||||
github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||||
|
github.com/duo-labs/webauthn v0.0.0-20211221191814-a22482edaa3b h1:L63RATZFZuFMXy6ixnKmv3eNAXwYQF6HW1vd4IYsQqQ=
|
||||||
|
github.com/duo-labs/webauthn v0.0.0-20211221191814-a22482edaa3b/go.mod h1:EYSpSkwoEcryMmQGfhol2IiB3IMN9IIIaNd/wcAQMGQ=
|
||||||
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||||
github.com/elastic/go-elasticsearch/v6 v6.8.5/go.mod h1:UwaDJsD3rWLM5rKNFzv9hgox93HoX8utj1kxD9aFUcI=
|
github.com/elastic/go-elasticsearch/v6 v6.8.5/go.mod h1:UwaDJsD3rWLM5rKNFzv9hgox93HoX8utj1kxD9aFUcI=
|
||||||
github.com/elazarl/go-bindata-assetfs v1.0.0 h1:G/bYguwHIzWq9ZoyUQqrjTmJbbYn3j3CKKpKinvZLFk=
|
github.com/elazarl/go-bindata-assetfs v1.0.0 h1:G/bYguwHIzWq9ZoyUQqrjTmJbbYn3j3CKKpKinvZLFk=
|
||||||
@ -135,6 +139,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
|
|||||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk=
|
github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk=
|
||||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/fxamacker/cbor/v2 v2.2.0 h1:6eXqdDDe588rSYAi1HfZKbx6YYQO4mxQ9eC6xYpU/JQ=
|
||||||
|
github.com/fxamacker/cbor/v2 v2.2.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
|
||||||
github.com/glendc/gopher-json v0.0.0-20170414221815-dc4743023d0c/go.mod h1:Gja1A+xZ9BoviGJNA2E9vFkPjjsl+CoJxSXiQM1UXtw=
|
github.com/glendc/gopher-json v0.0.0-20170414221815-dc4743023d0c/go.mod h1:Gja1A+xZ9BoviGJNA2E9vFkPjjsl+CoJxSXiQM1UXtw=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8=
|
github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||||
@ -164,6 +170,7 @@ github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB
|
|||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
|
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||||
github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU=
|
github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU=
|
||||||
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||||
@ -201,6 +208,8 @@ github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNu
|
|||||||
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
|
github.com/google/certificate-transparency-go v1.0.21 h1:Yf1aXowfZ2nuboBsg7iYGLmwsOARdV86pfH3g95wXmE=
|
||||||
|
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
@ -298,6 +307,8 @@ github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJK
|
|||||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
@ -397,6 +408,8 @@ github.com/ugorji/go v0.0.0-20171122102828-84cb69a8af83/go.mod h1:hnLbHMwcvSihnD
|
|||||||
github.com/volcengine/volc-sdk-golang v1.0.19 h1:jJp+aJgK0e//rZ9I0K2Y7ufJwvuZRo/AQsYDynXMNgA=
|
github.com/volcengine/volc-sdk-golang v1.0.19 h1:jJp+aJgK0e//rZ9I0K2Y7ufJwvuZRo/AQsYDynXMNgA=
|
||||||
github.com/volcengine/volc-sdk-golang v1.0.19/go.mod h1:+GGi447k4p1I5PNdbpG2GLaF0Ui9vIInTojMM0IfSS4=
|
github.com/volcengine/volc-sdk-golang v1.0.19/go.mod h1:+GGi447k4p1I5PNdbpG2GLaF0Ui9vIInTojMM0IfSS4=
|
||||||
github.com/wendal/errors v0.0.0-20130201093226-f66c77a7882b/go.mod h1:Q12BUT7DqIlHRmgv3RskH+UCM/4eqVMgI0EMmlSpAXc=
|
github.com/wendal/errors v0.0.0-20130201093226-f66c77a7882b/go.mod h1:Q12BUT7DqIlHRmgv3RskH+UCM/4eqVMgI0EMmlSpAXc=
|
||||||
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
@ -413,6 +426,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
|||||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
@ -47,6 +47,7 @@ type Application struct {
|
|||||||
EnableSigninSession bool `json:"enableSigninSession"`
|
EnableSigninSession bool `json:"enableSigninSession"`
|
||||||
EnableCodeSignin bool `json:"enableCodeSignin"`
|
EnableCodeSignin bool `json:"enableCodeSignin"`
|
||||||
EnableSamlCompress bool `json:"enableSamlCompress"`
|
EnableSamlCompress bool `json:"enableSamlCompress"`
|
||||||
|
EnableWebAuthn bool `json:"enableWebAuthn"`
|
||||||
Providers []*ProviderItem `xorm:"mediumtext" json:"providers"`
|
Providers []*ProviderItem `xorm:"mediumtext" json:"providers"`
|
||||||
SignupItems []*SignupItem `xorm:"varchar(1000)" json:"signupItems"`
|
SignupItems []*SignupItem `xorm:"varchar(1000)" json:"signupItems"`
|
||||||
GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"`
|
GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"`
|
||||||
|
@ -15,9 +15,11 @@
|
|||||||
package object
|
package object
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/gob"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
|
||||||
"github.com/casdoor/casdoor/util"
|
"github.com/casdoor/casdoor/util"
|
||||||
|
"github.com/duo-labs/webauthn/webauthn"
|
||||||
)
|
)
|
||||||
|
|
||||||
func InitDb() {
|
func InitDb() {
|
||||||
@ -29,6 +31,8 @@ func InitDb() {
|
|||||||
initBuiltInCert()
|
initBuiltInCert()
|
||||||
initBuiltInLdap()
|
initBuiltInLdap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initWebAuthn()
|
||||||
}
|
}
|
||||||
|
|
||||||
func initBuiltInOrganization() bool {
|
func initBuiltInOrganization() bool {
|
||||||
@ -72,6 +76,7 @@ func initBuiltInOrganization() bool {
|
|||||||
{Name: "Is global admin", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
|
{Name: "Is global admin", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
|
||||||
{Name: "Is forbidden", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
|
{Name: "Is forbidden", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
|
||||||
{Name: "Is deleted", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
|
{Name: "Is deleted", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
|
||||||
|
{Name: "WebAuthn credentials", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
AddOrganization(organization)
|
AddOrganization(organization)
|
||||||
@ -221,3 +226,7 @@ func initBuiltInProvider() {
|
|||||||
}
|
}
|
||||||
AddProvider(provider)
|
AddProvider(provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func initWebAuthn() {
|
||||||
|
gob.Register(webauthn.SessionData{})
|
||||||
|
}
|
||||||
|
@ -20,6 +20,7 @@ import (
|
|||||||
|
|
||||||
"github.com/casdoor/casdoor/conf"
|
"github.com/casdoor/casdoor/conf"
|
||||||
"github.com/casdoor/casdoor/util"
|
"github.com/casdoor/casdoor/util"
|
||||||
|
"github.com/duo-labs/webauthn/webauthn"
|
||||||
"xorm.io/core"
|
"xorm.io/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -99,6 +100,8 @@ type User struct {
|
|||||||
Douyin string `xorm:"douyin vachar(100)" json:"douyin"`
|
Douyin string `xorm:"douyin vachar(100)" json:"douyin"`
|
||||||
Custom string `xorm:"custom varchar(100)" json:"custom"`
|
Custom string `xorm:"custom varchar(100)" json:"custom"`
|
||||||
|
|
||||||
|
WebauthnCredentials []webauthn.Credential `xorm:"webauthnCredentials blob" json:"webauthnCredentials"`
|
||||||
|
|
||||||
Ldap string `xorm:"ldap varchar(100)" json:"ldap"`
|
Ldap string `xorm:"ldap varchar(100)" json:"ldap"`
|
||||||
Properties map[string]string `json:"properties"`
|
Properties map[string]string `json:"properties"`
|
||||||
}
|
}
|
||||||
@ -328,7 +331,7 @@ func UpdateUser(id string, user *User, columns []string, isGlobalAdmin bool) boo
|
|||||||
if len(columns) == 0 {
|
if len(columns) == 0 {
|
||||||
columns = []string{"owner", "display_name", "avatar",
|
columns = []string{"owner", "display_name", "avatar",
|
||||||
"location", "address", "region", "language", "affiliation", "title", "homepage", "bio", "score", "tag", "signup_application",
|
"location", "address", "region", "language", "affiliation", "title", "homepage", "bio", "score", "tag", "signup_application",
|
||||||
"is_admin", "is_global_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties"}
|
"is_admin", "is_global_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials"}
|
||||||
}
|
}
|
||||||
if isGlobalAdmin {
|
if isGlobalAdmin {
|
||||||
columns = append(columns, "name", "email", "phone")
|
columns = append(columns, "name", "email", "phone")
|
||||||
|
102
object/user_webauthn.go
Normal file
102
object/user_webauthn.go
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
// Copyright 2022 The casbin Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed 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 object
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/astaxie/beego"
|
||||||
|
"github.com/duo-labs/webauthn/protocol"
|
||||||
|
"github.com/duo-labs/webauthn/webauthn"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetWebAuthnObject(host string) *webauthn.WebAuthn {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
origin := beego.AppConfig.String("origin")
|
||||||
|
if origin == "" {
|
||||||
|
_, origin = getOriginFromHost(host)
|
||||||
|
}
|
||||||
|
|
||||||
|
localUrl, err := url.Parse(origin)
|
||||||
|
if err != nil {
|
||||||
|
panic("error when parsing origin:" + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
webAuthn, err := webauthn.New(&webauthn.Config{
|
||||||
|
RPDisplayName: beego.AppConfig.String("appname"), // Display Name for your site
|
||||||
|
RPID: strings.Split(localUrl.Host, ":")[0], // Generally the domain name for your site, it's ok because splits cannot return empty array
|
||||||
|
RPOrigin: origin, // The origin URL for WebAuthn requests
|
||||||
|
// RPIcon: "https://duo.com/logo.png", // Optional icon URL for your site
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return webAuthn
|
||||||
|
}
|
||||||
|
|
||||||
|
// implementation of webauthn.User interface
|
||||||
|
func (u *User) WebAuthnID() []byte {
|
||||||
|
return []byte(u.GetId())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) WebAuthnName() string {
|
||||||
|
return u.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) WebAuthnDisplayName() string {
|
||||||
|
return u.DisplayName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) WebAuthnCredentials() []webauthn.Credential {
|
||||||
|
return u.WebauthnCredentials
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) WebAuthnIcon() string {
|
||||||
|
return u.Avatar
|
||||||
|
}
|
||||||
|
|
||||||
|
// CredentialExcludeList returns a CredentialDescriptor array filled with all the user's credentials
|
||||||
|
func (u *User) CredentialExcludeList() []protocol.CredentialDescriptor {
|
||||||
|
credentials := u.WebAuthnCredentials()
|
||||||
|
credentialExcludeList := []protocol.CredentialDescriptor{}
|
||||||
|
for _, cred := range credentials {
|
||||||
|
descriptor := protocol.CredentialDescriptor{
|
||||||
|
Type: protocol.PublicKeyCredentialType,
|
||||||
|
CredentialID: cred.ID,
|
||||||
|
}
|
||||||
|
credentialExcludeList = append(credentialExcludeList, descriptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
return credentialExcludeList
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) AddCredentials(credential webauthn.Credential, isGlobalAdmin bool) bool {
|
||||||
|
u.WebauthnCredentials = append(u.WebauthnCredentials, credential)
|
||||||
|
return UpdateUser(u.GetId(), u, []string{"webauthnCredentials"}, isGlobalAdmin)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) DeleteCredentials(credentialIdBase64 string) bool {
|
||||||
|
for i, credential := range u.WebauthnCredentials {
|
||||||
|
if base64.StdEncoding.EncodeToString(credential.ID) == credentialIdBase64 {
|
||||||
|
u.WebauthnCredentials = append(u.WebauthnCredentials[0:i], u.WebauthnCredentials[i+1:]...)
|
||||||
|
return UpdateUserForAllFields(u.GetId(), u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
@ -109,6 +109,10 @@ func getUrlPath(urlPath string) string {
|
|||||||
return "/api/login/oauth"
|
return "/api/login/oauth"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(urlPath, "/api/webauthn") {
|
||||||
|
return "/api/webauthn"
|
||||||
|
}
|
||||||
|
|
||||||
return urlPath
|
return urlPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,4 +191,9 @@ func initAPI() {
|
|||||||
beego.Router("/cas/:organization/:application/p3/proxyValidate", &controllers.RootController{}, "GET:CasP3ServiceAndProxyValidate")
|
beego.Router("/cas/:organization/:application/p3/proxyValidate", &controllers.RootController{}, "GET:CasP3ServiceAndProxyValidate")
|
||||||
beego.Router("/cas/:organization/:application/samlValidate", &controllers.RootController{}, "POST:SamlValidate")
|
beego.Router("/cas/:organization/:application/samlValidate", &controllers.RootController{}, "POST:SamlValidate")
|
||||||
|
|
||||||
|
beego.Router("/api/webauthn/signup/begin", &controllers.ApiController{}, "Get:WebAuthnSignupBegin")
|
||||||
|
beego.Router("/api/webauthn/signup/finish", &controllers.ApiController{}, "Post:WebAuthnSignupFinish")
|
||||||
|
beego.Router("/api/webauthn/signin/begin", &controllers.ApiController{}, "Get:WebAuthnSigninBegin")
|
||||||
|
beego.Router("/api/webauthn/signin/finish", &controllers.ApiController{}, "Post:WebAuthnSigninFinish")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -92,6 +92,7 @@ class AccountTable extends React.Component {
|
|||||||
{name: "Is global admin", displayName: i18next.t("user:Is global admin")},
|
{name: "Is global admin", displayName: i18next.t("user:Is global admin")},
|
||||||
{name: "Is forbidden", displayName: i18next.t("user:Is forbidden")},
|
{name: "Is forbidden", displayName: i18next.t("user:Is forbidden")},
|
||||||
{name: "Is deleted", displayName: i18next.t("user:Is deleted")},
|
{name: "Is deleted", displayName: i18next.t("user:Is deleted")},
|
||||||
|
{name: "WebAuthn credentials", displayName: i18next.t("user:WebAuthn credentials")},
|
||||||
];
|
];
|
||||||
|
|
||||||
const getItemDisplayName = (text) => {
|
const getItemDisplayName = (text) => {
|
||||||
|
@ -353,6 +353,16 @@ class ApplicationEditPage extends React.Component {
|
|||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("application:Enable WebAuthn signin"), i18next.t("application:Enable WebAuthn signin - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={1} >
|
||||||
|
<Switch checked={this.state.application.enableWebAuthn} onChange={checked => {
|
||||||
|
this.updateApplicationField("enableWebAuthn", checked);
|
||||||
|
}} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("general:Signup URL"), i18next.t("general:Signup URL - Tooltip"))} :
|
{Setting.getLabel(i18next.t("general:Signup URL"), i18next.t("general:Signup URL - Tooltip"))} :
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {Button, Card, Col, Input, Result, Row, Select, Spin, Switch} from "antd";
|
import {Button, Card, Col, Input, Result, Row, Select, Spin, Switch} from "antd";
|
||||||
import * as UserBackend from "./backend/UserBackend";
|
import * as UserBackend from "./backend/UserBackend";
|
||||||
|
import * as UserWebauthnBackend from "./backend/UserWebauthnBackend";
|
||||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||||
import * as Setting from "./Setting";
|
import * as Setting from "./Setting";
|
||||||
import {LinkOutlined} from "@ant-design/icons";
|
import {LinkOutlined} from "@ant-design/icons";
|
||||||
@ -27,6 +28,7 @@ import AffiliationSelect from "./common/AffiliationSelect";
|
|||||||
import OAuthWidget from "./common/OAuthWidget";
|
import OAuthWidget from "./common/OAuthWidget";
|
||||||
import SamlWidget from "./common/SamlWidget";
|
import SamlWidget from "./common/SamlWidget";
|
||||||
import SelectRegionBox from "./SelectRegionBox";
|
import SelectRegionBox from "./SelectRegionBox";
|
||||||
|
import WebAuthnCredentialTable from "./WebauthnCredentialTable";
|
||||||
|
|
||||||
import {Controlled as CodeMirror} from "react-codemirror2";
|
import {Controlled as CodeMirror} from "react-codemirror2";
|
||||||
import "codemirror/lib/codemirror.css";
|
import "codemirror/lib/codemirror.css";
|
||||||
@ -515,6 +517,17 @@ class UserEditPage extends React.Component {
|
|||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
)
|
||||||
|
} else if(accountItem.name === "WebAuthn credentials") {
|
||||||
|
return (
|
||||||
|
<Row style={{marginTop: '20px'}} >
|
||||||
|
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("user:WebAuthn credentials"), i18next.t("user:WebAuthn credentials"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={22} >
|
||||||
|
<WebAuthnCredentialTable table={this.state.user.webauthnCredentials} updateTable={(table)=>{this.updateUserField('webauthnCredentials',table)}} refresh={this.getUser.bind(this)}/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
72
web/src/WebauthnCredentialTable.js
Normal file
72
web/src/WebauthnCredentialTable.js
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed 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.
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import {Button, Table} from 'antd';
|
||||||
|
import i18next from "i18next";
|
||||||
|
import * as UserWebauthnBackend from "./backend/UserWebauthnBackend";
|
||||||
|
import * as Setting from "./Setting";
|
||||||
|
|
||||||
|
class WebAuthnCredentialTable extends React.Component {
|
||||||
|
render() {
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: i18next.t("user:WebAuthn credentials"),
|
||||||
|
dataIndex: 'ID',
|
||||||
|
key: 'ID',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18next.t("general:Action"),
|
||||||
|
key: 'action',
|
||||||
|
render: (text, record, index) => {
|
||||||
|
return (
|
||||||
|
<Button style={{marginTop: '5px', marginBottom: '5px', marginRight: '5px'}} type="danger" onClick={() => {this.deleteRow(this.props.table, index)}}>
|
||||||
|
{i18next.t("general:Delete")}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
return (
|
||||||
|
<Table scroll={{x: 'max-content'}} rowKey={"ID"} columns={columns} dataSource={this.props.table} size="middle" bordered pagination={false}
|
||||||
|
title={() => (
|
||||||
|
<div>
|
||||||
|
{i18next.t("user:WebAuthn credentials")}
|
||||||
|
<Button style={{marginRight: "5px"}} type="primary" size="small" onClick={() => {this.registerWebAuthn()}}>{i18next.t("general:Add")}</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteRow(table, i) {
|
||||||
|
table = Setting.deleteRow(table, i);
|
||||||
|
this.props.updateTable(table);
|
||||||
|
}
|
||||||
|
|
||||||
|
registerWebAuthn() {
|
||||||
|
UserWebauthnBackend.registerWebauthnCredential().then((res) => {
|
||||||
|
if (res.msg === "") {
|
||||||
|
Setting.showMessage("success", `Successfully added webauthn credentials`);
|
||||||
|
} else {
|
||||||
|
Setting.showMessage("error", res.msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.refresh();
|
||||||
|
}).catch(error => {
|
||||||
|
Setting.showMessage("error", `Failed to connect to server: ${error}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WebAuthnCredentialTable;
|
@ -14,8 +14,9 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {Link} from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
import {Button, Checkbox, Col, Form, Input, Result, Row, Spin} from "antd";
|
import {Button, Checkbox, Col, Form, Input, Result, Row, Spin, Tabs} from "antd";
|
||||||
import {LockOutlined, UserOutlined} from "@ant-design/icons";
|
import {LockOutlined, UserOutlined} from "@ant-design/icons";
|
||||||
|
import * as UserWebauthnBackend from "../backend/UserWebauthnBackend";
|
||||||
import * as AuthBackend from "./AuthBackend";
|
import * as AuthBackend from "./AuthBackend";
|
||||||
import * as ApplicationBackend from "../backend/ApplicationBackend";
|
import * as ApplicationBackend from "../backend/ApplicationBackend";
|
||||||
import * as Provider from "./Provider";
|
import * as Provider from "./Provider";
|
||||||
@ -49,6 +50,8 @@ import CustomGithubCorner from "../CustomGithubCorner";
|
|||||||
import {CountDownInput} from "../common/CountDownInput";
|
import {CountDownInput} from "../common/CountDownInput";
|
||||||
import BilibiliLoginButton from "./BilibiliLoginButton";
|
import BilibiliLoginButton from "./BilibiliLoginButton";
|
||||||
|
|
||||||
|
const { TabPane } = Tabs;
|
||||||
|
|
||||||
class LoginPage extends React.Component {
|
class LoginPage extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -65,7 +68,9 @@ class LoginPage extends React.Component {
|
|||||||
validEmailOrPhone: false,
|
validEmailOrPhone: false,
|
||||||
validEmail: false,
|
validEmail: false,
|
||||||
validPhone: false,
|
validPhone: false,
|
||||||
|
loginMethod: "password"
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.state.type === "cas" && props.match?.params.casApplicationName !== undefined) {
|
if (this.state.type === "cas" && props.match?.params.casApplicationName !== undefined) {
|
||||||
this.state.owner = props.match?.params.owner;
|
this.state.owner = props.match?.params.owner;
|
||||||
this.state.applicationName = props.match?.params.casApplicationName;
|
this.state.applicationName = props.match?.params.casApplicationName;
|
||||||
@ -407,6 +412,7 @@ class LoginPage extends React.Component {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
{this.renderMethodChoiceBox()}
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="username"
|
name="username"
|
||||||
rules={[
|
rules={[
|
||||||
@ -421,13 +427,15 @@ class LoginPage extends React.Component {
|
|||||||
this.setState({validEmailOrPhone: false});
|
this.setState({validEmailOrPhone: false});
|
||||||
return Promise.reject(i18next.t("login:The input is not valid Email or Phone!"));
|
return Promise.reject(i18next.t("login:The input is not valid Email or Phone!"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Setting.isValidPhone(this.state.username)) {
|
||||||
|
this.setState({validPhone: true});
|
||||||
|
}
|
||||||
|
if (Setting.isValidEmail(this.state.username)) {
|
||||||
|
this.setState({validEmail: true});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (Setting.isValidPhone(this.state.username)) {
|
|
||||||
this.setState({validPhone: true});
|
|
||||||
}
|
|
||||||
if (Setting.isValidEmail(this.state.username)) {
|
|
||||||
this.setState({validEmail: true});
|
|
||||||
}
|
|
||||||
this.setState({validEmailOrPhone: true});
|
this.setState({validEmailOrPhone: true});
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
@ -446,29 +454,7 @@ class LoginPage extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{
|
{
|
||||||
this.state.isCodeSignin ? (
|
this.renderPasswordOrCodeInput()
|
||||||
<Form.Item
|
|
||||||
name="code"
|
|
||||||
rules={[{required: true, message: i18next.t("login:Please input your code!")}]}
|
|
||||||
>
|
|
||||||
<CountDownInput
|
|
||||||
disabled={this.state.username?.length === 0 || !this.state.validEmailOrPhone}
|
|
||||||
onButtonClickArgs={[this.state.username, this.state.validEmail ? "email" : "phone", Setting.getApplicationName(application)]}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
) : (
|
|
||||||
<Form.Item
|
|
||||||
name="password"
|
|
||||||
rules={[{required: true, message: i18next.t("login:Please input your password!")}]}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
prefix={<LockOutlined className="site-form-item-icon" />}
|
|
||||||
type="password"
|
|
||||||
placeholder={i18next.t("login:Password")}
|
|
||||||
disabled={!application.enablePassword}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Form.Item name="autoSignin" valuePropName="checked" noStyle>
|
<Form.Item name="autoSignin" valuePropName="checked" noStyle>
|
||||||
@ -483,14 +469,24 @@ class LoginPage extends React.Component {
|
|||||||
</a>
|
</a>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Button
|
{
|
||||||
type="primary"
|
this.state.loginMethod === "password" ?
|
||||||
htmlType="submit"
|
(
|
||||||
style={{width: "100%", marginBottom: "5px"}}
|
<Button
|
||||||
disabled={!application.enablePassword}
|
type="primary"
|
||||||
>
|
htmlType="submit"
|
||||||
{i18next.t("login:Sign In")}
|
style={{width: "100%", marginBottom: '5px'}}
|
||||||
</Button>
|
disabled={!application.enablePassword}
|
||||||
|
>
|
||||||
|
{i18next.t("login:Sign In")}
|
||||||
|
</Button>
|
||||||
|
) :
|
||||||
|
(
|
||||||
|
<Button type="primary" style={{width: "100%", marginBottom: '5px'}} onClick={() => this.signInWithWebAuthn()}>
|
||||||
|
{i18next.t("login:Sign in with WebAuthn")}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
{
|
{
|
||||||
!application.enableSignUp ? null : this.renderFooter(application)
|
!application.enableSignUp ? null : this.renderFooter(application)
|
||||||
}
|
}
|
||||||
@ -624,6 +620,113 @@ class LoginPage extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signInWithWebAuthn() {
|
||||||
|
if (this.state.username === null || this.state.username === "") {
|
||||||
|
Setting.showMessage("error", "username is required for webauthn login");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let application = this.getApplicationObj();
|
||||||
|
return fetch(`${Setting.ServerUrl}/api/webauthn/signin/begin?owner=${application.organization}&name=${this.state.username}`, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include"
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then((credentialRequestOptions) => {
|
||||||
|
if ("status" in credentialRequestOptions) {
|
||||||
|
Setting.showMessage("error", credentialRequestOptions.msg);
|
||||||
|
throw credentialRequestOptions.status.msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
credentialRequestOptions.publicKey.challenge = UserWebauthnBackend.webAuthnBufferDecode(credentialRequestOptions.publicKey.challenge);
|
||||||
|
credentialRequestOptions.publicKey.allowCredentials.forEach(function (listItem) {
|
||||||
|
listItem.id = UserWebauthnBackend.webAuthnBufferDecode(listItem.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
return navigator.credentials.get({
|
||||||
|
publicKey: credentialRequestOptions.publicKey
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then((assertion) => {
|
||||||
|
let authData = assertion.response.authenticatorData;
|
||||||
|
let clientDataJSON = assertion.response.clientDataJSON;
|
||||||
|
let rawId = assertion.rawId;
|
||||||
|
let sig = assertion.response.signature;
|
||||||
|
let userHandle = assertion.response.userHandle;
|
||||||
|
return fetch(`${Setting.ServerUrl}/api/webauthn/signin/finish`, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: assertion.id,
|
||||||
|
rawId: UserWebauthnBackend.webAuthnBufferEncode(rawId),
|
||||||
|
type: assertion.type,
|
||||||
|
response: {
|
||||||
|
authenticatorData: UserWebauthnBackend.webAuthnBufferEncode(authData),
|
||||||
|
clientDataJSON: UserWebauthnBackend.webAuthnBufferEncode(clientDataJSON),
|
||||||
|
signature: UserWebauthnBackend.webAuthnBufferEncode(sig),
|
||||||
|
userHandle: UserWebauthnBackend.webAuthnBufferEncode(userHandle),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(res => res.json()).then((res) => {
|
||||||
|
if (res.msg === "") {
|
||||||
|
Setting.showMessage("success", `Successfully logged in with webauthn credentials`);
|
||||||
|
Setting.goToLink("/");
|
||||||
|
} else {
|
||||||
|
Setting.showMessage("error", res.msg);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
Setting.showMessage("error", `Failed to connect to server: ${error}`);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPasswordOrCodeInput() {
|
||||||
|
let application = this.getApplicationObj();
|
||||||
|
if (this.state.loginMethod === "password") {
|
||||||
|
return this.state.isCodeSignin ? (
|
||||||
|
<Form.Item
|
||||||
|
name="code"
|
||||||
|
rules={[{required: true, message: i18next.t("login:Please input your code!")}]}
|
||||||
|
>
|
||||||
|
<CountDownInput
|
||||||
|
disabled={this.state.username?.length === 0 || !this.state.validEmailOrPhone}
|
||||||
|
onButtonClickArgs={[this.state.username, "", Setting.getApplicationOrgName(application), true]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
) : (
|
||||||
|
<Form.Item
|
||||||
|
name="password"
|
||||||
|
rules={[{required: true, message: i18next.t("login:Please input your password!")}]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
prefix={<LockOutlined className="site-form-item-icon" />}
|
||||||
|
type="password"
|
||||||
|
placeholder={i18next.t("login:Password")}
|
||||||
|
disabled={!application.enablePassword}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMethodChoiceBox(){
|
||||||
|
let application = this.getApplicationObj();
|
||||||
|
if (application.enableWebAuthn) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Tabs defaultActiveKey="password" onChange={(key)=>{this.setState({loginMethod: key})}} centered>
|
||||||
|
<TabPane tab={i18next.t("login:Password")} key="password">
|
||||||
|
</TabPane>
|
||||||
|
<TabPane tab={"WebAuthn"} key="webAuthn">
|
||||||
|
</TabPane>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const application = this.getApplicationObj();
|
const application = this.getApplicationObj();
|
||||||
if (application === null) {
|
if (application === null) {
|
||||||
|
78
web/src/backend/UserWebauthnBackend.js
Normal file
78
web/src/backend/UserWebauthnBackend.js
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
// Copyright 2022 The Casdoor Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed 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.
|
||||||
|
|
||||||
|
import * as Setting from "../Setting";
|
||||||
|
|
||||||
|
export function registerWebauthnCredential() {
|
||||||
|
return fetch(`${Setting.ServerUrl}/api/webauthn/signup/begin`, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include"
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then((credentialCreationOptions) => {
|
||||||
|
credentialCreationOptions.publicKey.challenge = webAuthnBufferDecode(credentialCreationOptions.publicKey.challenge);
|
||||||
|
credentialCreationOptions.publicKey.user.id = webAuthnBufferDecode(credentialCreationOptions.publicKey.user.id);
|
||||||
|
if (credentialCreationOptions.publicKey.excludeCredentials) {
|
||||||
|
for (var i = 0; i < credentialCreationOptions.publicKey.excludeCredentials.length; i++) {
|
||||||
|
credentialCreationOptions.publicKey.excludeCredentials[i].id = webAuthnBufferDecode(credentialCreationOptions.publicKey.excludeCredentials[i].id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return navigator.credentials.create({
|
||||||
|
publicKey: credentialCreationOptions.publicKey
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then((credential) => {
|
||||||
|
let attestationObject = credential.response.attestationObject;
|
||||||
|
let clientDataJSON = credential.response.clientDataJSON;
|
||||||
|
let rawId = credential.rawId;
|
||||||
|
return fetch(`${Setting.ServerUrl}/api/webauthn/signup/finish`, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: credential.id,
|
||||||
|
rawId: webAuthnBufferEncode(rawId),
|
||||||
|
type: credential.type,
|
||||||
|
response: {
|
||||||
|
attestationObject: webAuthnBufferEncode(attestationObject),
|
||||||
|
clientDataJSON: webAuthnBufferEncode(clientDataJSON),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(res => res.json());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteUserWebAuthnCredential(credentialID) {
|
||||||
|
let form = new FormData()
|
||||||
|
form.append("credentialID", credentialID)
|
||||||
|
return fetch(`${Setting.ServerUrl}/api/webauthn/delete-credential`, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
body: form,
|
||||||
|
dataType: "text"
|
||||||
|
}).then(res => res.json())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base64 to ArrayBuffer
|
||||||
|
export function webAuthnBufferDecode(value) {
|
||||||
|
return Uint8Array.from(atob(value), c => c.charCodeAt(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArrayBuffer to URLBase64
|
||||||
|
export function webAuthnBufferEncode(value) {
|
||||||
|
return btoa(String.fromCharCode.apply(null, new Uint8Array(value)))
|
||||||
|
.replace(/\+/g, "-")
|
||||||
|
.replace(/\//g, "_")
|
||||||
|
.replace(/=/g, "");;
|
||||||
|
}
|
@ -13,6 +13,8 @@
|
|||||||
"Edit Application": "Anwendung bearbeiten",
|
"Edit Application": "Anwendung bearbeiten",
|
||||||
"Enable SAML compress": "Enable SAML compress",
|
"Enable SAML compress": "Enable SAML compress",
|
||||||
"Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip",
|
"Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip",
|
||||||
|
"Enable WebAuthn signin": "Enable WebAuthn signin",
|
||||||
|
"Enable WebAuthn signin - Tooltip": "Enable WebAuthn signin - Tooltip",
|
||||||
"Enable code signin": "Code-Anmeldung aktivieren",
|
"Enable code signin": "Code-Anmeldung aktivieren",
|
||||||
"Enable code signin - Tooltip": "Aktiviere Codeanmeldung - Tooltip",
|
"Enable code signin - Tooltip": "Aktiviere Codeanmeldung - Tooltip",
|
||||||
"Enable signin session - Tooltip": "Aktiviere Anmeldesession - Tooltip",
|
"Enable signin session - Tooltip": "Aktiviere Anmeldesession - Tooltip",
|
||||||
@ -248,6 +250,7 @@
|
|||||||
"Please input your password, at least 6 characters!": "Bitte geben Sie Ihr Passwort ein, mindestens 6 Zeichen!",
|
"Please input your password, at least 6 characters!": "Bitte geben Sie Ihr Passwort ein, mindestens 6 Zeichen!",
|
||||||
"Please input your username, Email or phone!": "Bitte geben Sie Ihren Benutzernamen, E-Mail oder Telefon ein!",
|
"Please input your username, Email or phone!": "Bitte geben Sie Ihren Benutzernamen, E-Mail oder Telefon ein!",
|
||||||
"Sign In": "Anmelden",
|
"Sign In": "Anmelden",
|
||||||
|
"Sign in with WebAuthn": "Sign in with WebAuthn",
|
||||||
"Sign in with code": "Mit Code anmelden",
|
"Sign in with code": "Mit Code anmelden",
|
||||||
"Sign in with password": "Mit Passwort anmelden",
|
"Sign in with password": "Mit Passwort anmelden",
|
||||||
"Sign in with {type}": "Mit {type} anmelden",
|
"Sign in with {type}": "Mit {type} anmelden",
|
||||||
@ -639,6 +642,7 @@
|
|||||||
"Unlink": "Link aufheben",
|
"Unlink": "Link aufheben",
|
||||||
"Upload (.xlsx)": "Upload (.xlsx)",
|
"Upload (.xlsx)": "Upload (.xlsx)",
|
||||||
"Upload a photo": "Foto hochladen",
|
"Upload a photo": "Foto hochladen",
|
||||||
|
"WebAuthn credentials": "WebAuthn credentials",
|
||||||
"input password": "Passwort eingeben"
|
"input password": "Passwort eingeben"
|
||||||
},
|
},
|
||||||
"webhook": {
|
"webhook": {
|
||||||
|
@ -13,6 +13,8 @@
|
|||||||
"Edit Application": "Edit Application",
|
"Edit Application": "Edit Application",
|
||||||
"Enable SAML compress": "Enable SAML compress",
|
"Enable SAML compress": "Enable SAML compress",
|
||||||
"Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip",
|
"Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip",
|
||||||
|
"Enable WebAuthn signin": "Enable WebAuthn signin",
|
||||||
|
"Enable WebAuthn signin - Tooltip": "Enable WebAuthn signin - Tooltip",
|
||||||
"Enable code signin": "Enable code signin",
|
"Enable code signin": "Enable code signin",
|
||||||
"Enable code signin - Tooltip": "Enable code signin - Tooltip",
|
"Enable code signin - Tooltip": "Enable code signin - Tooltip",
|
||||||
"Enable signin session - Tooltip": "Enable signin session - Tooltip",
|
"Enable signin session - Tooltip": "Enable signin session - Tooltip",
|
||||||
@ -248,6 +250,7 @@
|
|||||||
"Please input your password, at least 6 characters!": "Please input your password, at least 6 characters!",
|
"Please input your password, at least 6 characters!": "Please input your password, at least 6 characters!",
|
||||||
"Please input your username, Email or phone!": "Please input your username, Email or phone!",
|
"Please input your username, Email or phone!": "Please input your username, Email or phone!",
|
||||||
"Sign In": "Sign In",
|
"Sign In": "Sign In",
|
||||||
|
"Sign in with WebAuthn": "Sign in with WebAuthn",
|
||||||
"Sign in with code": "Sign in with code",
|
"Sign in with code": "Sign in with code",
|
||||||
"Sign in with password": "Sign in with password",
|
"Sign in with password": "Sign in with password",
|
||||||
"Sign in with {type}": "Sign in with {type}",
|
"Sign in with {type}": "Sign in with {type}",
|
||||||
@ -639,6 +642,7 @@
|
|||||||
"Unlink": "Unlink",
|
"Unlink": "Unlink",
|
||||||
"Upload (.xlsx)": "Upload (.xlsx)",
|
"Upload (.xlsx)": "Upload (.xlsx)",
|
||||||
"Upload a photo": "Upload a photo",
|
"Upload a photo": "Upload a photo",
|
||||||
|
"WebAuthn credentials": "WebAuthn credentials",
|
||||||
"input password": "input password"
|
"input password": "input password"
|
||||||
},
|
},
|
||||||
"webhook": {
|
"webhook": {
|
||||||
|
@ -13,6 +13,8 @@
|
|||||||
"Edit Application": "Modifier l'application",
|
"Edit Application": "Modifier l'application",
|
||||||
"Enable SAML compress": "Enable SAML compress",
|
"Enable SAML compress": "Enable SAML compress",
|
||||||
"Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip",
|
"Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip",
|
||||||
|
"Enable WebAuthn signin": "Enable WebAuthn signin",
|
||||||
|
"Enable WebAuthn signin - Tooltip": "Enable WebAuthn signin - Tooltip",
|
||||||
"Enable code signin": "Activer la connexion au code",
|
"Enable code signin": "Activer la connexion au code",
|
||||||
"Enable code signin - Tooltip": "Activer la connexion au code - infobulle",
|
"Enable code signin - Tooltip": "Activer la connexion au code - infobulle",
|
||||||
"Enable signin session - Tooltip": "Activer la session de connexion - infobulle",
|
"Enable signin session - Tooltip": "Activer la session de connexion - infobulle",
|
||||||
@ -248,6 +250,7 @@
|
|||||||
"Please input your password, at least 6 characters!": "Veuillez entrer votre mot de passe, au moins 6 caractères !",
|
"Please input your password, at least 6 characters!": "Veuillez entrer votre mot de passe, au moins 6 caractères !",
|
||||||
"Please input your username, Email or phone!": "Veuillez entrer votre nom d'utilisateur, votre e-mail ou votre téléphone!",
|
"Please input your username, Email or phone!": "Veuillez entrer votre nom d'utilisateur, votre e-mail ou votre téléphone!",
|
||||||
"Sign In": "Se connecter",
|
"Sign In": "Se connecter",
|
||||||
|
"Sign in with WebAuthn": "Sign in with WebAuthn",
|
||||||
"Sign in with code": "Se connecter avec le code",
|
"Sign in with code": "Se connecter avec le code",
|
||||||
"Sign in with password": "Se connecter avec le mot de passe",
|
"Sign in with password": "Se connecter avec le mot de passe",
|
||||||
"Sign in with {type}": "Se connecter avec {type}",
|
"Sign in with {type}": "Se connecter avec {type}",
|
||||||
@ -639,6 +642,7 @@
|
|||||||
"Unlink": "Délier",
|
"Unlink": "Délier",
|
||||||
"Upload (.xlsx)": "Télécharger (.xlsx)",
|
"Upload (.xlsx)": "Télécharger (.xlsx)",
|
||||||
"Upload a photo": "Télécharger une photo",
|
"Upload a photo": "Télécharger une photo",
|
||||||
|
"WebAuthn credentials": "WebAuthn credentials",
|
||||||
"input password": "saisir le mot de passe"
|
"input password": "saisir le mot de passe"
|
||||||
},
|
},
|
||||||
"webhook": {
|
"webhook": {
|
||||||
|
@ -13,6 +13,8 @@
|
|||||||
"Edit Application": "アプリケーションを編集",
|
"Edit Application": "アプリケーションを編集",
|
||||||
"Enable SAML compress": "Enable SAML compress",
|
"Enable SAML compress": "Enable SAML compress",
|
||||||
"Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip",
|
"Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip",
|
||||||
|
"Enable WebAuthn signin": "Enable WebAuthn signin",
|
||||||
|
"Enable WebAuthn signin - Tooltip": "Enable WebAuthn signin - Tooltip",
|
||||||
"Enable code signin": "コードサインインを有効にする",
|
"Enable code signin": "コードサインインを有効にする",
|
||||||
"Enable code signin - Tooltip": "Enable code signin - Tooltip",
|
"Enable code signin - Tooltip": "Enable code signin - Tooltip",
|
||||||
"Enable signin session - Tooltip": "Enable signin session - Tooltip",
|
"Enable signin session - Tooltip": "Enable signin session - Tooltip",
|
||||||
@ -248,6 +250,7 @@
|
|||||||
"Please input your password, at least 6 characters!": "6文字以上でパスワードを入力してください!",
|
"Please input your password, at least 6 characters!": "6文字以上でパスワードを入力してください!",
|
||||||
"Please input your username, Email or phone!": "ユーザー名、メールアドレスまたは電話番号を入力してください。",
|
"Please input your username, Email or phone!": "ユーザー名、メールアドレスまたは電話番号を入力してください。",
|
||||||
"Sign In": "サインイン",
|
"Sign In": "サインイン",
|
||||||
|
"Sign in with WebAuthn": "Sign in with WebAuthn",
|
||||||
"Sign in with code": "コードでサインイン",
|
"Sign in with code": "コードでサインイン",
|
||||||
"Sign in with password": "パスワードでサインイン",
|
"Sign in with password": "パスワードでサインイン",
|
||||||
"Sign in with {type}": "{type} でサインイン",
|
"Sign in with {type}": "{type} でサインイン",
|
||||||
@ -639,6 +642,7 @@
|
|||||||
"Unlink": "リンクを解除",
|
"Unlink": "リンクを解除",
|
||||||
"Upload (.xlsx)": "アップロード (.xlsx)",
|
"Upload (.xlsx)": "アップロード (.xlsx)",
|
||||||
"Upload a photo": "写真をアップロード",
|
"Upload a photo": "写真をアップロード",
|
||||||
|
"WebAuthn credentials": "WebAuthn credentials",
|
||||||
"input password": "パスワードを入力"
|
"input password": "パスワードを入力"
|
||||||
},
|
},
|
||||||
"webhook": {
|
"webhook": {
|
||||||
|
@ -13,6 +13,8 @@
|
|||||||
"Edit Application": "Edit Application",
|
"Edit Application": "Edit Application",
|
||||||
"Enable SAML compress": "Enable SAML compress",
|
"Enable SAML compress": "Enable SAML compress",
|
||||||
"Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip",
|
"Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip",
|
||||||
|
"Enable WebAuthn signin": "Enable WebAuthn signin",
|
||||||
|
"Enable WebAuthn signin - Tooltip": "Enable WebAuthn signin - Tooltip",
|
||||||
"Enable code signin": "Enable code signin",
|
"Enable code signin": "Enable code signin",
|
||||||
"Enable code signin - Tooltip": "Enable code signin - Tooltip",
|
"Enable code signin - Tooltip": "Enable code signin - Tooltip",
|
||||||
"Enable signin session - Tooltip": "Enable signin session - Tooltip",
|
"Enable signin session - Tooltip": "Enable signin session - Tooltip",
|
||||||
@ -248,6 +250,7 @@
|
|||||||
"Please input your password, at least 6 characters!": "Please input your password, at least 6 characters!",
|
"Please input your password, at least 6 characters!": "Please input your password, at least 6 characters!",
|
||||||
"Please input your username, Email or phone!": "Please input your username, Email or phone!",
|
"Please input your username, Email or phone!": "Please input your username, Email or phone!",
|
||||||
"Sign In": "Sign In",
|
"Sign In": "Sign In",
|
||||||
|
"Sign in with WebAuthn": "Sign in with WebAuthn",
|
||||||
"Sign in with code": "Sign in with code",
|
"Sign in with code": "Sign in with code",
|
||||||
"Sign in with password": "Sign in with password",
|
"Sign in with password": "Sign in with password",
|
||||||
"Sign in with {type}": "Sign in with {type}",
|
"Sign in with {type}": "Sign in with {type}",
|
||||||
@ -639,6 +642,7 @@
|
|||||||
"Unlink": "Unlink",
|
"Unlink": "Unlink",
|
||||||
"Upload (.xlsx)": "Upload (.xlsx)",
|
"Upload (.xlsx)": "Upload (.xlsx)",
|
||||||
"Upload a photo": "Upload a photo",
|
"Upload a photo": "Upload a photo",
|
||||||
|
"WebAuthn credentials": "WebAuthn credentials",
|
||||||
"input password": "input password"
|
"input password": "input password"
|
||||||
},
|
},
|
||||||
"webhook": {
|
"webhook": {
|
||||||
|
@ -13,6 +13,8 @@
|
|||||||
"Edit Application": "Изменить приложение",
|
"Edit Application": "Изменить приложение",
|
||||||
"Enable SAML compress": "Enable SAML compress",
|
"Enable SAML compress": "Enable SAML compress",
|
||||||
"Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip",
|
"Enable SAML compress - Tooltip": "Enable SAML compress - Tooltip",
|
||||||
|
"Enable WebAuthn signin": "Enable WebAuthn signin",
|
||||||
|
"Enable WebAuthn signin - Tooltip": "Enable WebAuthn signin - Tooltip",
|
||||||
"Enable code signin": "Включить кодовый вход",
|
"Enable code signin": "Включить кодовый вход",
|
||||||
"Enable code signin - Tooltip": "Включить вход с кодом - Tooltip",
|
"Enable code signin - Tooltip": "Включить вход с кодом - Tooltip",
|
||||||
"Enable signin session - Tooltip": "Включить сеанс входа - Подсказка",
|
"Enable signin session - Tooltip": "Включить сеанс входа - Подсказка",
|
||||||
@ -248,6 +250,7 @@
|
|||||||
"Please input your password, at least 6 characters!": "Пожалуйста, введите ваш пароль, по крайней мере 6 символов!",
|
"Please input your password, at least 6 characters!": "Пожалуйста, введите ваш пароль, по крайней мере 6 символов!",
|
||||||
"Please input your username, Email or phone!": "Пожалуйста, введите ваше имя пользователя, адрес электронной почты или телефон!",
|
"Please input your username, Email or phone!": "Пожалуйста, введите ваше имя пользователя, адрес электронной почты или телефон!",
|
||||||
"Sign In": "Войти",
|
"Sign In": "Войти",
|
||||||
|
"Sign in with WebAuthn": "Sign in with WebAuthn",
|
||||||
"Sign in with code": "Войти с помощью кода",
|
"Sign in with code": "Войти с помощью кода",
|
||||||
"Sign in with password": "Войти с помощью пароля",
|
"Sign in with password": "Войти с помощью пароля",
|
||||||
"Sign in with {type}": "Войти с помощью {type}",
|
"Sign in with {type}": "Войти с помощью {type}",
|
||||||
@ -639,6 +642,7 @@
|
|||||||
"Unlink": "Отвязать",
|
"Unlink": "Отвязать",
|
||||||
"Upload (.xlsx)": "Загрузить (.xlsx)",
|
"Upload (.xlsx)": "Загрузить (.xlsx)",
|
||||||
"Upload a photo": "Загрузить фото",
|
"Upload a photo": "Загрузить фото",
|
||||||
|
"WebAuthn credentials": "WebAuthn credentials",
|
||||||
"input password": "пароль для ввода"
|
"input password": "пароль для ввода"
|
||||||
},
|
},
|
||||||
"webhook": {
|
"webhook": {
|
||||||
|
@ -13,6 +13,8 @@
|
|||||||
"Edit Application": "编辑应用",
|
"Edit Application": "编辑应用",
|
||||||
"Enable SAML compress": "压缩SAML响应",
|
"Enable SAML compress": "压缩SAML响应",
|
||||||
"Enable SAML compress - Tooltip": "Casdoor作为SAML idp时,是否压缩SAML响应信息",
|
"Enable SAML compress - Tooltip": "Casdoor作为SAML idp时,是否压缩SAML响应信息",
|
||||||
|
"Enable WebAuthn signin": "Enable WebAuthn signin",
|
||||||
|
"Enable WebAuthn signin - Tooltip": "Enable WebAuthn signin - Tooltip",
|
||||||
"Enable code signin": "启用验证码登录",
|
"Enable code signin": "启用验证码登录",
|
||||||
"Enable code signin - Tooltip": "是否允许用手机或邮箱验证码登录",
|
"Enable code signin - Tooltip": "是否允许用手机或邮箱验证码登录",
|
||||||
"Enable signin session - Tooltip": "从应用登录Casdoor后,Casdoor是否保持会话",
|
"Enable signin session - Tooltip": "从应用登录Casdoor后,Casdoor是否保持会话",
|
||||||
@ -248,6 +250,7 @@
|
|||||||
"Please input your password, at least 6 characters!": "请输入您的密码,不少于6位",
|
"Please input your password, at least 6 characters!": "请输入您的密码,不少于6位",
|
||||||
"Please input your username, Email or phone!": "请输入您的用户名、Email或手机号!",
|
"Please input your username, Email or phone!": "请输入您的用户名、Email或手机号!",
|
||||||
"Sign In": "登录",
|
"Sign In": "登录",
|
||||||
|
"Sign in with WebAuthn": "Sign in with WebAuthn",
|
||||||
"Sign in with code": "验证码登录",
|
"Sign in with code": "验证码登录",
|
||||||
"Sign in with password": "密码登录",
|
"Sign in with password": "密码登录",
|
||||||
"Sign in with {type}": "{type}登录",
|
"Sign in with {type}": "{type}登录",
|
||||||
@ -639,6 +642,7 @@
|
|||||||
"Unlink": "解绑",
|
"Unlink": "解绑",
|
||||||
"Upload (.xlsx)": "上传(.xlsx)",
|
"Upload (.xlsx)": "上传(.xlsx)",
|
||||||
"Upload a photo": "上传头像",
|
"Upload a photo": "上传头像",
|
||||||
|
"WebAuthn credentials": "WebAuthn credentials",
|
||||||
"input password": "输入密码"
|
"input password": "输入密码"
|
||||||
},
|
},
|
||||||
"webhook": {
|
"webhook": {
|
||||||
|
Reference in New Issue
Block a user