Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions docs/passkey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//lint:file-ignore U1000 ignore go-swagger template
package docs

import (
"github.com/supabase/auth/internal/api"
)

// swagger:route POST /passkeys/sign-in user passkeySignIn
// Begin a passkey sign-in challenge.
// responses:
// 200: PasskeySignInChallengeResponse
// 400: BadRequestResponse
// 429: RateLimitResponse

type passkeySignInParamsWrapper struct {
// in:body
Body api.PasskeySignInRequest
}

// swagger:route POST /passkeys/sign-in/verify user passkeySignInVerify
// Complete a passkey sign-in challenge.
// responses:
// 200: AccessTokenResponseSchema
// 400: BadRequestResponse
// 429: RateLimitResponse

type passkeySignInVerifyParamsWrapper struct {
// in:body
Body api.PasskeySignInVerifyRequest
}

// swagger:route POST /passkeys user passkeyRegister
// Begin passkey registration for the authenticated user.
// responses:
// 200: PasskeyRegistrationResponse
// 400: BadRequestResponse
// 429: RateLimitResponse

type passkeyRegisterParamsWrapper struct {
// in:body
Body api.PasskeyRegistrationRequest
}

// swagger:route POST /passkeys/{passkey_id}/verify user passkeyVerify
// Verify a passkey registration challenge.
// responses:
// 200: PasskeyVerifyResponse
// 400: BadRequestResponse
// 429: RateLimitResponse

type passkeyVerifyParamsWrapper struct {
// in:body
Body api.PasskeyVerifyRequest
}
4 changes: 4 additions & 0 deletions example.env
Original file line number Diff line number Diff line change
Expand Up @@ -262,3 +262,7 @@ GOTRUE_SMS_TEST_OTP_VALID_UNTIL="<ISO date time>" # (e.g. 2023-09-29T08:14:06Z)

GOTRUE_MFA_WEB_AUTHN_ENROLL_ENABLED="false"
GOTRUE_MFA_WEB_AUTHN_VERIFY_ENABLED="false"

# Passkey configuration
GOTRUE_PASSKEY_ENABLED="false"
GOTRUE_PASSKEY_CHALLENGE_EXPIRY_DURATION="300"
22 changes: 22 additions & 0 deletions internal/api/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,28 @@ func (a *API) loadFactor(w http.ResponseWriter, r *http.Request) (context.Contex
return withFactor(ctx, factor), nil
}

func (a *API) loadPasskey(w http.ResponseWriter, r *http.Request) (context.Context, error) {
ctx := r.Context()
db := a.db.WithContext(ctx)
user := getUser(ctx)
passkeyID, err := uuid.FromString(chi.URLParam(r, "passkey_id"))
if err != nil {
return nil, apierrors.NewNotFoundError(apierrors.ErrorCodeValidationFailed, "passkey_id must be an UUID")
}

observability.LogEntrySetField(r, "passkey_id", passkeyID)

factor, err := user.FindOwnedPasskeyByID(db, passkeyID)
if err != nil {
if models.IsNotFoundError(err) {
return nil, apierrors.NewNotFoundError(apierrors.ErrorCodeMFAFactorNotFound, "Passkey not found")
}
return nil, apierrors.NewInternalServerError("Database error loading passkey").WithInternalError(err)
}

return withFactor(ctx, factor), nil
}

func (a *API) getAdminParams(r *http.Request) (*AdminUserParams, error) {
params := &AdminUserParams{}
if err := retrieveRequestParams(r, params); err != nil {
Expand Down
16 changes: 16 additions & 0 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,22 @@ func NewAPIWithVersion(globalConfig *conf.GlobalConfiguration, db *storage.Conne
})
})

r.Route("/passkeys", func(r *router) {
r.With(api.limitHandler(api.limiterOpts.PasskeySignIn)).Post("/sign-in", api.PasskeySignIn)
r.With(api.limitHandler(api.limiterOpts.PasskeySignIn)).Post("/sign-in/verify", api.PasskeySignInVerify)

r.With(api.requireAuthentication).With(api.requireNotAnonymous).Route("/", func(r *router) {
r.Get("/", api.ListPasskeys)
r.With(api.limitHandler(api.limiterOpts.PasskeyChallenge)).Post("/", api.CreatePasskey)

r.Route("/{passkey_id}", func(r *router) {
r.Use(api.loadPasskey)
r.With(api.limitHandler(api.limiterOpts.PasskeyChallenge)).Post("/verify", api.VerifyPasskey)
r.Delete("/", api.DeletePasskey)
})
})
})

r.Route("/sso", func(r *router) {
r.Use(api.requireSAMLEnabled)
r.With(api.limitHandler(api.limiterOpts.SSO)).
Expand Down
4 changes: 4 additions & 0 deletions internal/api/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ type RequestParams interface {
adminUserDeleteParams |
security.GotrueRequest |
ChallengeFactorParams |
PasskeyRegistrationRequest |
PasskeyVerifyRequest |
PasskeySignInRequest |
PasskeySignInVerifyRequest |

struct {
Email string `json:"email"`
Expand Down
6 changes: 5 additions & 1 deletion internal/api/mfa.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,14 @@ func validateFactors(db *storage.Connection, user *models.User, newFactorName st
if err := db.Load(user, "Factors"); err != nil {
return err
}
factorCount := len(user.Factors)
factorCount := 0
numVerifiedFactors := 0

for _, factor := range user.Factors {
if factor.IsPasskeyFactor() {
continue
}
factorCount++
if factor.FriendlyName == newFactorName {
return apierrors.NewUnprocessableEntityError(
apierrors.ErrorCodeMFAFactorNameConflict,
Expand Down
12 changes: 12 additions & 0 deletions internal/api/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ type LimiterOptions struct {
User *limiter.Limiter
FactorVerify *limiter.Limiter
FactorChallenge *limiter.Limiter
PasskeySignIn *limiter.Limiter
PasskeyChallenge *limiter.Limiter
SSO *limiter.Limiter
SAMLAssertion *limiter.Limiter
Web3 *limiter.Limiter
Expand Down Expand Up @@ -85,6 +87,16 @@ func NewLimiterOptions(gc *conf.GlobalConfiguration) *LimiterOptions {
DefaultExpirationTTL: time.Minute,
}).SetBurst(30)

o.PasskeySignIn = tollbooth.NewLimiter(gc.RateLimitVerify/(60*5),
&limiter.ExpirableOptions{
DefaultExpirationTTL: time.Hour,
}).SetBurst(30)

o.PasskeyChallenge = tollbooth.NewLimiter(gc.RateLimitVerify/(60*5),
&limiter.ExpirableOptions{
DefaultExpirationTTL: time.Hour,
}).SetBurst(30)

o.SSO = tollbooth.NewLimiter(gc.RateLimitSso/(60*5),
&limiter.ExpirableOptions{
DefaultExpirationTTL: time.Hour,
Expand Down
Loading