Skip to content
Merged
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ certs/
node_modules/*
*.code-workspace
.claude/worktrees
docs/superpowers
docs/superpowers
.worktrees/
373 changes: 361 additions & 12 deletions internal/graph/generated/generated.go

Large diffs are not rendered by default.

28 changes: 16 additions & 12 deletions internal/graph/model/models_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions internal/graph/schema.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ type Meta {
is_sms_otp_mfa_enabled: Boolean!
# is_webauthn_enabled indicates WebAuthn/passkey enrollment is available (always on; no operator flag).
is_webauthn_enabled: Boolean!
# is_mfa_enforced mirrors EnforceMFA — when true, the frontend must not
# offer a standalone passkey primary-login path (it would bypass the org's
# two-factor requirement); passkey should only be offered as a second
# factor after password/social login. The server enforces this
# independently in webauthn_login_verify regardless of what the frontend
# shows.
is_mfa_enforced: Boolean!
}

# AdminMeta is admin-only configuration metadata exposed via the _admin_meta
Expand Down Expand Up @@ -80,6 +87,9 @@ type User {
updated_at: Int64
revoked_timestamp: Int64
is_multi_factor_auth_enabled: Boolean
# has_skipped_mfa_setup_at is set once the user explicitly skips the
# optional MFA setup prompt shown at login. Null means never skipped.
has_skipped_mfa_setup_at: Int64
app_data: Map
}

Expand Down Expand Up @@ -115,6 +125,19 @@ type AuthResponse {
should_show_email_otp_screen: Boolean
should_show_mobile_otp_screen: Boolean
should_show_totp_screen: Boolean
# should_offer_webauthn_mfa_verify is true when the authenticated-with-
# password user has a registered passkey and MFA verification (not
# enrollment) is required before a token is issued. The frontend should
# offer a "verify with your passkey" action — calling the existing scoped
# webauthn_login_options(email)/webauthn_login_verify mutations — in
# addition to (or instead of) should_show_totp_screen's code-entry form.
should_offer_webauthn_mfa_verify: Boolean
# should_offer_mfa_setup is true when MFA is available but not enforced,
# the user hasn't enrolled, and they haven't skipped setup before. Unlike
# should_show_totp_screen, access_token is ALREADY populated alongside
# this flag — the frontend should log the user in and separately offer
# (not force) MFA setup, e.g. via a dismissible hub with a Skip action.
should_offer_mfa_setup: Boolean
access_token: String
id_token: String
refresh_token: String
Expand Down Expand Up @@ -1317,6 +1340,10 @@ type Mutation {
revoke(params: OAuthRevokeRequest!): Response!
verify_otp(params: VerifyOTPRequest!): AuthResponse!
resend_otp(params: ResendOTPRequest!): Response!
# skip_mfa_setup records that the authenticated caller explicitly declined
# the optional MFA setup prompt. Fails with FAILED_PRECONDITION if MFA is
# organization-enforced (enforce-mfa) — enforcement is never skippable.
skip_mfa_setup: Response!
# WebAuthn / passkey self-service ceremonies (no admin `_` prefix).
webauthn_registration_options(
email: String
Expand Down
5 changes: 5 additions & 0 deletions internal/graph/schema.resolvers.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions internal/graphql/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ type Provider interface {
// DeactivateAccount is the method to deactivate account.
// Permissions: authorized user
DeactivateAccount(ctx context.Context) (*model.Response, error)
// SkipMFASetup is the method to skip optional MFA setup.
// Permissions: authorized user
SkipMFASetup(ctx context.Context) (*model.Response, error)
// DeleteEmailTemplate is the method to delete email template.
// Permissions: authorizer:admin
DeleteEmailTemplate(ctx context.Context, params *model.DeleteEmailTemplateRequest) (*model.Response, error)
Expand Down
26 changes: 26 additions & 0 deletions internal/graphql/skip_mfa_setup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// internal/graphql/skip_mfa_setup.go
package graphql

import (
"context"

"github.com/authorizerdev/authorizer/internal/graph/model"
"github.com/authorizerdev/authorizer/internal/metrics"
"github.com/authorizerdev/authorizer/internal/service"
"github.com/authorizerdev/authorizer/internal/utils"
)

func (g *graphqlProvider) SkipMFASetup(ctx context.Context) (*model.Response, error) {
gc, err := utils.GinContextFromContext(ctx)
if err != nil {
g.Log.Debug().Err(err).Msg("failed to get gin context")
metrics.RecordSecurityEvent(metrics.SecurityEventGinContextMissing, "graphql")
return nil, err
}
res, side, err := g.ServiceProvider.SkipMFASetup(ctx, service.MetaFromGin(gc))
if err != nil {
return nil, err
}
service.ApplyToGin(gc, side)
return res, nil
}
18 changes: 18 additions & 0 deletions internal/integration_tests/meta_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,24 @@ func TestMeta(t *testing.T) {
assert.False(t, meta.IsBasicAuthenticationEnabled)
})

t.Run("should reflect enforced MFA", func(t *testing.T) {
cfg2 := getTestConfig()
cfg2.EnforceMFA = true
ts2 := initTestSetup(t, cfg2)
_, ctx2 := createContext(ts2)

meta, err := ts2.GraphQLProvider.Meta(ctx2)
require.NoError(t, err)
assert.NotNil(t, meta)
assert.True(t, meta.IsMfaEnforced)
})

t.Run("should reflect non-enforced MFA by default", func(t *testing.T) {
meta, err := ts.GraphQLProvider.Meta(ctx)
require.NoError(t, err)
assert.False(t, meta.IsMfaEnforced)
})

t.Run("should expose per-method MFA availability with default config", func(t *testing.T) {
meta, err := ts.GraphQLProvider.Meta(ctx)
require.NoError(t, err)
Expand Down
Loading
Loading