Skip to main content

Credentials

Password or arbitrary field-based sign-in. Mirrors Auth.js Credentials provider.

Important rules

  • Sessions are always JWT (or bearer tokens) — never database strategy.
  • Sign-in endpoint: POST /auth/callback/credentials (not GET).
  • Works with MFA globally or selectively from Authorize.

Simple example

import "github.com/izetmolla/goauth/providers/credentials"

credentials.New(credentials.Options{
Fields: []goauth.CredentialField{
{Name: "email", Label: "Email", Type: "email"},
{Name: "password", Label: "Password", Type: "password"},
},
Authorize: func(ctx context.Context, creds map[string]string, r *http.Request) (*goauth.User, error) {
u, ok := validatePassword(creds["email"], creds["password"])
if !ok {
return nil, nil // rejected sign-in
}
return u, nil
},
})
curl -X POST http://localhost:3000/auth/callback/credentials \
-d "email=demo@example.com&password=secret"

Selective MFA from Authorize

After a successful password check, opt in to MFA for specific users. Configure MFA.SendCode (adapter recommended). MFA.Enabled can stay false.

credentials.New(credentials.Options{
Authorize: func(ctx context.Context, creds map[string]string, r *http.Request) (*goauth.User, error) {
dbUser, err := api.Validate(creds["email"], creds["password"])
if dbUser == nil {
return nil, err
}

user := &goauth.User{ID: dbUser.ID, Email: dbUser.Email, Phone: dbUser.Phone}

if api.UserRequiresMFA(dbUser, r) {
return credentials.RequireMFA(
user,
goauth.VerificationEmail,
goauth.MaskEmail(dbUser.Email),
), nil
}

return user, nil
},
})

The client receives JSON with mfaRequired, channel, and destination — see Selective MFA guide.

Phone MFA

return credentials.RequireMFA(
user,
goauth.VerificationPhone,
goauth.MaskPhone(user.Phone),
user.Phone,
), nil

Manual flags

user.RequireMFA = true
user.MFADelivery = &goauth.MFADeliveryHint{
Channel: goauth.VerificationEmail,
Destination: goauth.MaskEmail(user.Email),
}
return user, nil

Global MFA + token flow

When MFA.Enabled: true, every login is challenged (unless device trusted):

# Step 1 — password OK, MFA required
curl -X POST http://localhost:3000/auth/callback/credentials \
-H "X-Auth-Flow: token" \
-H "Accept: application/json" \
-d "email=demo@example.com&password=secret"
# → { "challenge": "eyJ...", "expiresIn": 600, "mfaRequired": true, "channel": "email", "destination": "d***@example.com" }

# Step 2 — verify OTP
curl -X POST http://localhost:3000/auth/mfa/verify \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "challenge=eyJ...&code=123456&trustDevice=true"
# → { accessToken, refreshToken, sessionId, user }

Authorize contract

ReturnMeaning
(*User, nil)Success — session issued (or MFA challenge if RequireMFA)
(nil, nil)Invalid credentials (generic failure)
(nil, err)CredentialsSignin error
User fieldSet in AuthorizeEffect
RequireMFAtrueTriggers MFA after password OK
MFADeliveryChannel + masked destinationReturned in challenge JSON

Never put passwords in JWT claims — only run Authorize server-side.