diff --git a/authz/authz.go b/authz/authz.go
index bbae3e2a..607a0c3c 100644
--- a/authz/authz.go
+++ b/authz/authz.go
@@ -105,6 +105,7 @@ p, *, *, GET, /api/get-saml-login, *, *
p, *, *, POST, /api/acs, *, *
p, *, *, GET, /api/saml/metadata, *, *
p, *, *, *, /cas, *, *
+p, *, *, *, /api/webauthn, *, *
`
sa := stringadapter.NewAdapter(ruleText)
diff --git a/controllers/webauthn.go b/controllers/webauthn.go
new file mode 100644
index 00000000..474436e2
--- /dev/null
+++ b/controllers/webauthn.go
@@ -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)
+}
diff --git a/go.mod b/go.mod
index 0c169385..00d2ed6c 100644
--- a/go.mod
+++ b/go.mod
@@ -14,6 +14,7 @@ require (
github.com/casdoor/goth v1.69.0-FIX2
github.com/casdoor/oss v1.2.0
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-ldap/ldap/v3 v3.3.0
github.com/go-pay/gopay v1.5.72
diff --git a/go.sum b/go.sum
index f06f0013..bbca77da 100644
--- a/go.sum
+++ b/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/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
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/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=
@@ -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/go.mod h1:QGrK8vMWWHQYQ3QU9bw9Y9OPNfxccGzfb41qjvVeXtY=
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/elastic/go-elasticsearch/v6 v6.8.5/go.mod h1:UwaDJsD3rWLM5rKNFzv9hgox93HoX8utj1kxD9aFUcI=
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/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
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/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=
@@ -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/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/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/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
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/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/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.3.0/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/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/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-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
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/go.mod h1:+GGi447k4p1I5PNdbpG2GLaF0Ui9vIInTojMM0IfSS4=
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.27/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-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-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-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
diff --git a/object/application.go b/object/application.go
index fa4f6f78..d2e3a2d1 100644
--- a/object/application.go
+++ b/object/application.go
@@ -47,6 +47,7 @@ type Application struct {
EnableSigninSession bool `json:"enableSigninSession"`
EnableCodeSignin bool `json:"enableCodeSignin"`
EnableSamlCompress bool `json:"enableSamlCompress"`
+ EnableWebAuthn bool `json:"enableWebAuthn"`
Providers []*ProviderItem `xorm:"mediumtext" json:"providers"`
SignupItems []*SignupItem `xorm:"varchar(1000)" json:"signupItems"`
GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"`
diff --git a/object/init.go b/object/init.go
index 14c3365c..1e25acd7 100644
--- a/object/init.go
+++ b/object/init.go
@@ -15,9 +15,11 @@
package object
import (
+ "encoding/gob"
"io/ioutil"
"github.com/casdoor/casdoor/util"
+ "github.com/duo-labs/webauthn/webauthn"
)
func InitDb() {
@@ -29,6 +31,8 @@ func InitDb() {
initBuiltInCert()
initBuiltInLdap()
}
+
+ initWebAuthn()
}
func initBuiltInOrganization() bool {
@@ -72,6 +76,7 @@ func initBuiltInOrganization() bool {
{Name: "Is global admin", 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: "WebAuthn credentials", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
},
}
AddOrganization(organization)
@@ -221,3 +226,7 @@ func initBuiltInProvider() {
}
AddProvider(provider)
}
+
+func initWebAuthn() {
+ gob.Register(webauthn.SessionData{})
+}
diff --git a/object/user.go b/object/user.go
index 2ddd2b9b..1d08b86f 100644
--- a/object/user.go
+++ b/object/user.go
@@ -20,6 +20,7 @@ import (
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util"
+ "github.com/duo-labs/webauthn/webauthn"
"xorm.io/core"
)
@@ -99,6 +100,8 @@ type User struct {
Douyin string `xorm:"douyin vachar(100)" json:"douyin"`
Custom string `xorm:"custom varchar(100)" json:"custom"`
+ WebauthnCredentials []webauthn.Credential `xorm:"webauthnCredentials blob" json:"webauthnCredentials"`
+
Ldap string `xorm:"ldap varchar(100)" json:"ldap"`
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 {
columns = []string{"owner", "display_name", "avatar",
"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 {
columns = append(columns, "name", "email", "phone")
diff --git a/object/user_webauthn.go b/object/user_webauthn.go
new file mode 100644
index 00000000..e3c25821
--- /dev/null
+++ b/object/user_webauthn.go
@@ -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
+}
diff --git a/routers/authz_filter.go b/routers/authz_filter.go
index c6d3a935..4070ae38 100644
--- a/routers/authz_filter.go
+++ b/routers/authz_filter.go
@@ -109,6 +109,10 @@ func getUrlPath(urlPath string) string {
return "/api/login/oauth"
}
+ if strings.HasPrefix(urlPath, "/api/webauthn") {
+ return "/api/webauthn"
+ }
+
return urlPath
}
diff --git a/routers/router.go b/routers/router.go
index 509742ad..bcc55cbc 100644
--- a/routers/router.go
+++ b/routers/router.go
@@ -191,4 +191,9 @@ func initAPI() {
beego.Router("/cas/:organization/:application/p3/proxyValidate", &controllers.RootController{}, "GET:CasP3ServiceAndProxyValidate")
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")
+
}
diff --git a/web/src/AccountTable.js b/web/src/AccountTable.js
index 93502982..58da2648 100644
--- a/web/src/AccountTable.js
+++ b/web/src/AccountTable.js
@@ -92,6 +92,7 @@ class AccountTable extends React.Component {
{name: "Is global admin", displayName: i18next.t("user:Is global admin")},
{name: "Is forbidden", displayName: i18next.t("user:Is forbidden")},
{name: "Is deleted", displayName: i18next.t("user:Is deleted")},
+ {name: "WebAuthn credentials", displayName: i18next.t("user:WebAuthn credentials")},
];
const getItemDisplayName = (text) => {
diff --git a/web/src/ApplicationEditPage.js b/web/src/ApplicationEditPage.js
index c49f948d..84fe13d8 100644
--- a/web/src/ApplicationEditPage.js
+++ b/web/src/ApplicationEditPage.js
@@ -353,6 +353,16 @@ class ApplicationEditPage extends React.Component {
}} />
+