mirror of
https://github.com/netbirdio/dex.git
synced 2026-05-22 18:43:53 -07:00
Two-Factor authentication (TOTP) (#3712)
Signed-off-by: maksim.nabokikh <max.nabokih@gmail.com>
This commit is contained in:
@@ -69,6 +69,19 @@ type Config struct {
|
||||
// Sessions holds authentication session configuration.
|
||||
// Requires DEX_SESSIONS_ENABLED=true feature flag.
|
||||
Sessions *Sessions `json:"sessions"`
|
||||
|
||||
// MFA holds multi-factor authentication configuration.
|
||||
MFA MFAConfig `json:"mfa"`
|
||||
}
|
||||
|
||||
// MFAConfig holds multi-factor authentication settings.
|
||||
type MFAConfig struct {
|
||||
// Authenticators defines MFA providers available for clients to reference.
|
||||
Authenticators []MFAAuthenticator `json:"authenticators"`
|
||||
|
||||
// DefaultMFAChain is the default ordered list of authenticator IDs applied
|
||||
// to clients that don't specify their own mfaChain. Empty means no MFA by default.
|
||||
DefaultMFAChain []string `json:"defaultMFAChain"`
|
||||
}
|
||||
|
||||
// Validate the configuration
|
||||
@@ -112,6 +125,54 @@ func (c Config) Validate() error {
|
||||
return fmt.Errorf("sessions config requires sessions to be enabled (DEX_SESSIONS_ENABLED=true)")
|
||||
}
|
||||
|
||||
if err := c.validateMFA(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Config) validateMFA() error {
|
||||
mfa := c.MFA
|
||||
if len(mfa.Authenticators) == 0 && len(mfa.DefaultMFAChain) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !featureflags.SessionsEnabled.Enabled() {
|
||||
return fmt.Errorf("mfa requires sessions to be enabled (DEX_SESSIONS_ENABLED=true)")
|
||||
}
|
||||
|
||||
knownTypes := map[string]bool{"TOTP": true}
|
||||
ids := make(map[string]bool, len(mfa.Authenticators))
|
||||
|
||||
for _, auth := range mfa.Authenticators {
|
||||
if auth.ID == "" {
|
||||
return fmt.Errorf("mfa.authenticators: authenticator must have an id")
|
||||
}
|
||||
if ids[auth.ID] {
|
||||
return fmt.Errorf("mfa.authenticators: duplicate authenticator id %q", auth.ID)
|
||||
}
|
||||
ids[auth.ID] = true
|
||||
|
||||
if !knownTypes[auth.Type] {
|
||||
return fmt.Errorf("mfa.authenticators: unknown type %q for authenticator %q", auth.Type, auth.ID)
|
||||
}
|
||||
}
|
||||
|
||||
for _, authID := range mfa.DefaultMFAChain {
|
||||
if !ids[authID] {
|
||||
return fmt.Errorf("mfa.defaultMFAChain: references unknown authenticator %q", authID)
|
||||
}
|
||||
}
|
||||
|
||||
for _, client := range c.StaticClients {
|
||||
for _, authID := range client.MFAChain {
|
||||
if !ids[authID] {
|
||||
return fmt.Errorf("staticClients: client %q references unknown MFA authenticator %q", client.ID, authID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -606,3 +667,20 @@ type Sessions struct {
|
||||
// RememberMeCheckedByDefault controls the default state of the "remember me" checkbox.
|
||||
RememberMeCheckedByDefault *bool `json:"rememberMeCheckedByDefault"`
|
||||
}
|
||||
|
||||
// MFAAuthenticator defines a multi-factor authentication provider.
|
||||
type MFAAuthenticator struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Config json.RawMessage `json:"config"`
|
||||
|
||||
// ConnectorTypes limits this authenticator to specific connector types (e.g., "ldap", "oidc", "saml").
|
||||
// If empty, the authenticator applies to all connector types.
|
||||
ConnectorTypes []string `json:"connectorTypes"`
|
||||
}
|
||||
|
||||
// TOTPConfig holds configuration for a TOTP authenticator.
|
||||
type TOTPConfig struct {
|
||||
// Issuer is the name of the service shown in the authenticator app.
|
||||
Issuer string `json:"issuer"`
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
@@ -384,6 +385,8 @@ func runServe(options serveOptions) error {
|
||||
ContinueOnConnectorFailure: featureflags.ContinueOnConnectorFailure.Enabled(),
|
||||
Signer: signerInstance,
|
||||
IDTokensValidFor: idTokensValidFor,
|
||||
MFAProviders: buildMFAProviders(c.MFA.Authenticators, logger),
|
||||
DefaultMFAChain: c.MFA.DefaultMFAChain,
|
||||
}
|
||||
|
||||
if c.Expiry.AuthRequests != "" {
|
||||
@@ -813,3 +816,26 @@ func parseSessionConfig(s *Sessions) (*server.SessionConfig, error) {
|
||||
}
|
||||
return sc, nil
|
||||
}
|
||||
|
||||
func buildMFAProviders(authenticators []MFAAuthenticator, logger *slog.Logger) map[string]server.MFAProvider {
|
||||
if len(authenticators) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
providers := make(map[string]server.MFAProvider, len(authenticators))
|
||||
for _, auth := range authenticators {
|
||||
switch auth.Type {
|
||||
case "TOTP":
|
||||
var cfg TOTPConfig
|
||||
if err := json.Unmarshal(auth.Config, &cfg); err != nil {
|
||||
logger.Error("failed to parse TOTP config", "id", auth.ID, "err", err)
|
||||
continue
|
||||
}
|
||||
providers[auth.ID] = server.NewTOTPProvider(cfg.Issuer, auth.ConnectorTypes)
|
||||
logger.Info("MFA authenticator configured", "id", auth.ID, "type", auth.Type)
|
||||
default:
|
||||
logger.Error("unknown MFA authenticator type, skipping", "id", auth.ID, "type", auth.Type)
|
||||
}
|
||||
}
|
||||
return providers
|
||||
}
|
||||
|
||||
@@ -138,6 +138,25 @@ telemetry:
|
||||
# # Supported code challenge methods. Defaults to ["S256", "plain"].
|
||||
# codeChallengeMethodsSupported: ["S256", "plain"]
|
||||
|
||||
# Multi-factor authentication configuration.
|
||||
# Requires DEX_SESSIONS_ENABLED=true feature flag.
|
||||
mfa:
|
||||
authenticators:
|
||||
- id: totp-1
|
||||
type: TOTP
|
||||
config:
|
||||
issuer: "dex-1"
|
||||
# # Optional: limit this authenticator to specific connector types (e.g., ldap, oidc, saml).
|
||||
# # If omitted or empty, applies to all connector types.
|
||||
# # It is recommended to use this option to prevent MFA from being used for connectors
|
||||
# # with their own MFA mechanisms, e.g., OIDC, Google, etc. (but technically, it is possible).
|
||||
# connectorTypes:
|
||||
# - mockCallback
|
||||
# # Default MFA chain applied to clients that don't specify their own mfaChain.
|
||||
# # If omitted or empty, no MFA is required by default.
|
||||
# defaultMFAChain:
|
||||
# - totp-1
|
||||
|
||||
# Instead of reading from an external storage, use this list of clients.
|
||||
#
|
||||
# If this option isn't chosen clients may be added through the gRPC API.
|
||||
@@ -152,6 +171,11 @@ staticClients:
|
||||
# If omitted or empty, all connectors are allowed.
|
||||
# allowedConnectors:
|
||||
# - mock
|
||||
# Optional: ordered list of MFA authenticator IDs the user must complete during login.
|
||||
# References authenticator IDs from mfa.authenticators.
|
||||
# If omitted, mfa.defaultMFAChain is used.
|
||||
# mfaChain:
|
||||
# - totp-1
|
||||
|
||||
# Example using environment variables
|
||||
# Set DEX_CLIENT_ID and DEX_SECURE_CLIENT_SECRET before starting Dex
|
||||
|
||||
@@ -28,6 +28,7 @@ require (
|
||||
github.com/oklog/run v1.2.0
|
||||
github.com/openbao/openbao/api/v2 v2.5.1
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/russellhaering/goxmldsig v1.5.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
@@ -58,6 +59,7 @@ require (
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bmatcuk/doublestar v1.3.4 // indirect
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/coreos/go-semver v0.3.1 // indirect
|
||||
|
||||
@@ -42,6 +42,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=
|
||||
github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
@@ -201,6 +203,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
|
||||
+60
-9
@@ -789,7 +789,41 @@ func (s *Server) finalizeLogin(ctx context.Context, identity connector.Identity,
|
||||
userIdentity = &ui
|
||||
}
|
||||
|
||||
// we can skip the redirect to /approval and go ahead and send code if it's not required
|
||||
// an HMAC is used here to ensure that the request ID is unpredictable, ensuring that an attacker who intercepted the original
|
||||
// flow would be unable to poll for the result at the /approval endpoint
|
||||
h := hmac.New(sha256.New, authReq.HMACKey)
|
||||
h.Write([]byte(authReq.ID))
|
||||
mac := h.Sum(nil)
|
||||
hmacParam := base64.RawURLEncoding.EncodeToString(mac)
|
||||
|
||||
// Check if the client requires MFA.
|
||||
mfaChain, err := s.mfaChainForClient(ctx, authReq.ClientID, authReq.ConnectorID)
|
||||
if err != nil {
|
||||
return "", false, fmt.Errorf("failed to get MFA chain for client: %v", err)
|
||||
}
|
||||
if len(mfaChain) > 0 {
|
||||
// Redirect to MFA verification starting with the first authenticator.
|
||||
// Each authenticator redirects to the next one in the chain upon success.
|
||||
// HMAC includes authenticatorID to prevent skipping steps by URL manipulation.
|
||||
h.Reset()
|
||||
h.Write([]byte(authReq.ID + "|" + mfaChain[0]))
|
||||
v := url.Values{}
|
||||
v.Set("req", authReq.ID)
|
||||
v.Set("hmac", base64.RawURLEncoding.EncodeToString(h.Sum(nil)))
|
||||
v.Set("authenticator", mfaChain[0])
|
||||
returnURL := path.Join(s.issuerURL.Path, "/mfa/verify") + "?" + v.Encode()
|
||||
return returnURL, false, nil
|
||||
}
|
||||
|
||||
// No MFA required — mark as validated.
|
||||
if err := s.storage.UpdateAuthRequest(ctx, authReq.ID, func(a storage.AuthRequest) (storage.AuthRequest, error) {
|
||||
a.MFAValidated = true
|
||||
return a, nil
|
||||
}); err != nil {
|
||||
return "", false, fmt.Errorf("failed to update auth request MFA status: %v", err)
|
||||
}
|
||||
|
||||
// Skip approval if globally configured.
|
||||
if s.skipApproval && !authReq.ForceApprovalPrompt {
|
||||
return "", true, nil
|
||||
}
|
||||
@@ -801,13 +835,7 @@ func (s *Server) finalizeLogin(ctx context.Context, identity connector.Identity,
|
||||
}
|
||||
}
|
||||
|
||||
// an HMAC is used here to ensure that the request ID is unpredictable, ensuring that an attacker who intercepted the original
|
||||
// flow would be unable to poll for the result at the /approval endpoint
|
||||
h := hmac.New(sha256.New, authReq.HMACKey)
|
||||
h.Write([]byte(authReq.ID))
|
||||
mac := h.Sum(nil)
|
||||
|
||||
returnURL := path.Join(s.issuerURL.Path, "/approval") + "?req=" + authReq.ID + "&hmac=" + base64.RawURLEncoding.EncodeToString(mac)
|
||||
returnURL := path.Join(s.issuerURL.Path, "/approval") + "?req=" + authReq.ID + "&hmac=" + hmacParam
|
||||
return returnURL, false, nil
|
||||
}
|
||||
|
||||
@@ -840,8 +868,31 @@ func (s *Server) handleApproval(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// build expected hmac with secret key
|
||||
h := hmac.New(sha256.New, authReq.HMACKey)
|
||||
if !authReq.MFAValidated {
|
||||
// Check if MFA is actually required — if so, redirect to TOTP instead of blocking.
|
||||
// This handles the case where MFA was enabled after the auth flow started.
|
||||
mfaChain, err := s.mfaChainForClient(ctx, authReq.ClientID, authReq.ConnectorID)
|
||||
if err != nil {
|
||||
s.logger.ErrorContext(ctx, "failed to get MFA chain", "err", err)
|
||||
s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
|
||||
return
|
||||
}
|
||||
if len(mfaChain) > 0 {
|
||||
h.Write([]byte(authReq.ID + "|" + mfaChain[0]))
|
||||
v := url.Values{}
|
||||
v.Set("req", authReq.ID)
|
||||
v.Set("hmac", base64.RawURLEncoding.EncodeToString(h.Sum(nil)))
|
||||
v.Set("authenticator", mfaChain[0])
|
||||
h.Reset()
|
||||
totpURL := path.Join(s.issuerURL.Path, "/mfa/verify") + "?" + v.Encode()
|
||||
http.Redirect(w, r, totpURL, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
// No MFA required but flag not set — allow through (backward compat).
|
||||
}
|
||||
|
||||
// build expected hmac with secret key
|
||||
h.Write([]byte(authReq.ID))
|
||||
expectedMAC := h.Sum(nil)
|
||||
// constant time comparison
|
||||
|
||||
@@ -84,6 +84,7 @@ func TestHandleApprovalDoubleSubmitPOST(t *testing.T) {
|
||||
RedirectURI: "https://client.example/callback",
|
||||
Expiry: time.Now().Add(time.Minute),
|
||||
LoggedIn: true,
|
||||
MFAValidated: true,
|
||||
HMACKey: []byte("approval-double-submit-key"),
|
||||
}
|
||||
require.NoError(t, server.storage.CreateAuthRequest(ctx, authReq))
|
||||
|
||||
+364
@@ -0,0 +1,364 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"image/png"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"github.com/pquerna/otp"
|
||||
"github.com/pquerna/otp/totp"
|
||||
|
||||
"github.com/dexidp/dex/storage"
|
||||
)
|
||||
|
||||
// MFAProvider is a pluggable multi-factor authentication method.
|
||||
type MFAProvider interface {
|
||||
// Type returns the authenticator type identifier (e.g., "TOTP").
|
||||
Type() string
|
||||
// EnabledForConnectorType returns true if this provider applies to the given connector type.
|
||||
// If no connector types are configured, the provider applies to all.
|
||||
EnabledForConnectorType(connectorType string) bool
|
||||
}
|
||||
|
||||
// TOTPProvider implements TOTP-based multi-factor authentication.
|
||||
type TOTPProvider struct {
|
||||
issuer string
|
||||
connectorTypes map[string]struct{}
|
||||
}
|
||||
|
||||
// NewTOTPProvider creates a new TOTP MFA provider.
|
||||
func NewTOTPProvider(issuer string, connectorTypes []string) *TOTPProvider {
|
||||
m := make(map[string]struct{}, len(connectorTypes))
|
||||
for _, t := range connectorTypes {
|
||||
m[t] = struct{}{}
|
||||
}
|
||||
return &TOTPProvider{issuer: issuer, connectorTypes: m}
|
||||
}
|
||||
|
||||
func (p *TOTPProvider) EnabledForConnectorType(connectorType string) bool {
|
||||
if len(p.connectorTypes) == 0 {
|
||||
return true
|
||||
}
|
||||
_, ok := p.connectorTypes[connectorType]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (p *TOTPProvider) Type() string { return "TOTP" }
|
||||
|
||||
func (p *TOTPProvider) generate(connID, email string) (*otp.Key, error) {
|
||||
return totp.Generate(totp.GenerateOpts{
|
||||
Issuer: p.issuer,
|
||||
AccountName: fmt.Sprintf("(%s) %s", connID, email),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleMFAVerify(w http.ResponseWriter, r *http.Request) {
|
||||
macEncoded := r.FormValue("hmac")
|
||||
if macEncoded == "" {
|
||||
s.renderError(r, w, http.StatusUnauthorized, "Unauthorized request.")
|
||||
return
|
||||
}
|
||||
mac, err := base64.RawURLEncoding.DecodeString(macEncoded)
|
||||
if err != nil {
|
||||
s.renderError(r, w, http.StatusUnauthorized, "Unauthorized request.")
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
authReq, err := s.storage.GetAuthRequest(ctx, r.FormValue("req"))
|
||||
if err != nil {
|
||||
s.logger.ErrorContext(ctx, "failed to get auth request", "err", err)
|
||||
s.renderError(r, w, http.StatusInternalServerError, "Database error.")
|
||||
return
|
||||
}
|
||||
if !authReq.LoggedIn {
|
||||
s.logger.ErrorContext(ctx, "auth request does not have an identity for MFA verification")
|
||||
s.renderError(r, w, http.StatusInternalServerError, "Login process not yet finalized.")
|
||||
return
|
||||
}
|
||||
|
||||
authenticatorID := r.FormValue("authenticator")
|
||||
|
||||
// Verify HMAC — includes authenticatorID to prevent skipping steps in the MFA chain.
|
||||
h := hmac.New(sha256.New, authReq.HMACKey)
|
||||
h.Write([]byte(authReq.ID + "|" + authenticatorID))
|
||||
if !hmac.Equal(mac, h.Sum(nil)) {
|
||||
s.renderError(r, w, http.StatusUnauthorized, "Unauthorized request.")
|
||||
return
|
||||
}
|
||||
provider, ok := s.mfaProviders[authenticatorID]
|
||||
if !ok {
|
||||
s.renderError(r, w, http.StatusBadRequest, "Unknown authenticator.")
|
||||
return
|
||||
}
|
||||
|
||||
totpProvider, ok := provider.(*TOTPProvider)
|
||||
if !ok {
|
||||
s.renderError(r, w, http.StatusInternalServerError, "Unsupported authenticator type.")
|
||||
return
|
||||
}
|
||||
|
||||
identity, err := s.storage.GetUserIdentity(ctx, authReq.Claims.UserID, authReq.ConnectorID)
|
||||
if err != nil {
|
||||
s.logger.ErrorContext(ctx, "failed to get user identity", "err", err)
|
||||
s.renderError(r, w, http.StatusInternalServerError, "Database error.")
|
||||
return
|
||||
}
|
||||
|
||||
// Build approval URL with an HMAC that covers only the request ID
|
||||
// (MFA HMAC includes authenticatorID and is not valid for approval).
|
||||
approvalH := hmac.New(sha256.New, authReq.HMACKey)
|
||||
approvalH.Write([]byte(authReq.ID))
|
||||
returnURL := path.Join(s.issuerURL.Path, "/approval") + "?req=" + authReq.ID +
|
||||
"&hmac=" + base64.RawURLEncoding.EncodeToString(approvalH.Sum(nil))
|
||||
|
||||
if authReq.MFAValidated {
|
||||
http.Redirect(w, r, returnURL, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
secret := identity.MFASecrets[authenticatorID]
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
if secret == nil {
|
||||
// First-time enrollment: generate a new TOTP key.
|
||||
// TODO(nabokihms): clean up stale unconfirmed secrets. If a user starts
|
||||
// enrollment multiple times without completing it, old secrets accumulate.
|
||||
generated, err := totpProvider.generate(authReq.ConnectorID, authReq.Claims.Email)
|
||||
if err != nil {
|
||||
s.logger.ErrorContext(ctx, "failed to generate TOTP key", "err", err)
|
||||
s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
|
||||
return
|
||||
}
|
||||
|
||||
secret = &storage.MFASecret{
|
||||
AuthenticatorID: authenticatorID,
|
||||
Type: "TOTP",
|
||||
Secret: generated.String(),
|
||||
Confirmed: false,
|
||||
CreatedAt: s.now(),
|
||||
}
|
||||
|
||||
if err := s.storage.UpdateUserIdentity(ctx, authReq.Claims.UserID, authReq.ConnectorID, func(old storage.UserIdentity) (storage.UserIdentity, error) {
|
||||
if old.MFASecrets == nil {
|
||||
old.MFASecrets = make(map[string]*storage.MFASecret)
|
||||
}
|
||||
old.MFASecrets[authenticatorID] = secret
|
||||
return old, nil
|
||||
}); err != nil {
|
||||
s.logger.ErrorContext(ctx, "failed to store MFA secret", "err", err)
|
||||
s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
s.renderTOTPPage(secret, false, totpProvider.issuer, authReq.ConnectorID, w, r)
|
||||
|
||||
case http.MethodPost:
|
||||
// TODO(nabokihms): this endpoint should be protected with a rate limit (like the auth endpoint).
|
||||
// TOTP has a limited keyspace (6 digits) with a 30-second validity window,
|
||||
// making it particularly vulnerable to brute-force without rate limiting.
|
||||
//
|
||||
// For now the best way is to use external rate limiting solutions.
|
||||
if secret == nil || secret.Secret == "" {
|
||||
s.renderError(r, w, http.StatusBadRequest, "MFA not enrolled.")
|
||||
return
|
||||
}
|
||||
|
||||
code := r.FormValue("totp")
|
||||
generated, err := otp.NewKeyFromURL(secret.Secret)
|
||||
if err != nil {
|
||||
s.logger.ErrorContext(ctx, "failed to load TOTP key", "err", err)
|
||||
s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
|
||||
return
|
||||
}
|
||||
|
||||
if !totp.Validate(code, generated.Secret()) {
|
||||
s.renderTOTPPage(secret, true, totpProvider.issuer, authReq.ConnectorID, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Mark MFA secret as confirmed.
|
||||
if !secret.Confirmed {
|
||||
if err := s.storage.UpdateUserIdentity(ctx, authReq.Claims.UserID, authReq.ConnectorID, func(old storage.UserIdentity) (storage.UserIdentity, error) {
|
||||
if s := old.MFASecrets[authenticatorID]; s != nil {
|
||||
s.Confirmed = true
|
||||
}
|
||||
return old, nil
|
||||
}); err != nil {
|
||||
s.logger.ErrorContext(ctx, "failed to confirm MFA secret", "err", err)
|
||||
s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there are more authenticators in the MFA chain.
|
||||
mfaChain, err := s.mfaChainForClient(ctx, authReq.ClientID, authReq.ConnectorID)
|
||||
if err != nil {
|
||||
s.logger.ErrorContext(ctx, "failed to get MFA chain", "err", err)
|
||||
s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
|
||||
return
|
||||
}
|
||||
|
||||
// Find the next authenticator in the chain after the current one.
|
||||
var nextAuthenticator string
|
||||
for i, id := range mfaChain {
|
||||
if id == authenticatorID && i+1 < len(mfaChain) {
|
||||
nextAuthenticator = mfaChain[i+1]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if nextAuthenticator != "" {
|
||||
// Redirect to the next authenticator in the chain.
|
||||
h := hmac.New(sha256.New, authReq.HMACKey)
|
||||
h.Write([]byte(authReq.ID + "|" + nextAuthenticator))
|
||||
v := url.Values{}
|
||||
v.Set("req", authReq.ID)
|
||||
v.Set("hmac", base64.RawURLEncoding.EncodeToString(h.Sum(nil)))
|
||||
v.Set("authenticator", nextAuthenticator)
|
||||
nextURL := path.Join(s.issuerURL.Path, "/mfa/verify") + "?" + v.Encode()
|
||||
http.Redirect(w, r, nextURL, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// All authenticators in the chain completed — mark as validated.
|
||||
if err := s.storage.UpdateAuthRequest(ctx, authReq.ID, func(old storage.AuthRequest) (storage.AuthRequest, error) {
|
||||
old.MFAValidated = true
|
||||
return old, nil
|
||||
}); err != nil {
|
||||
s.logger.ErrorContext(ctx, "failed to update auth request", "err", err)
|
||||
s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
|
||||
return
|
||||
}
|
||||
|
||||
s.sendCodeOrRedirectToApproval(w, r, authReq, returnURL)
|
||||
|
||||
default:
|
||||
s.renderError(r, w, http.StatusBadRequest, "Unsupported request method.")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) renderTOTPPage(secret *storage.MFASecret, lastFail bool, issuer, connectorID string, w http.ResponseWriter, r *http.Request) {
|
||||
// Prevent browser from caching the TOTP page (contains QR code with secret).
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
var qrCode string
|
||||
if !secret.Confirmed {
|
||||
var err error
|
||||
qrCode, err = generateTOTPQRCode(secret.Secret)
|
||||
if err != nil {
|
||||
s.logger.ErrorContext(r.Context(), "failed to generate QR code", "err", err)
|
||||
s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := s.templates.totpVerify(r, w, r.URL.String(), issuer, connectorID, qrCode, lastFail); err != nil {
|
||||
s.logger.ErrorContext(r.Context(), "server template error", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// sendCodeOrRedirectToApproval checks skipApproval and stored consent,
|
||||
// sending a code response directly if possible, or redirecting to the approval page.
|
||||
func (s *Server) sendCodeOrRedirectToApproval(w http.ResponseWriter, r *http.Request, authReq storage.AuthRequest, approvalURL string) {
|
||||
ctx := r.Context()
|
||||
|
||||
if !authReq.ForceApprovalPrompt {
|
||||
if s.skipApproval {
|
||||
authReq, err := s.storage.GetAuthRequest(ctx, authReq.ID)
|
||||
if err != nil {
|
||||
s.logger.ErrorContext(ctx, "failed to get auth request", "err", err)
|
||||
s.renderError(r, w, http.StatusInternalServerError, "Login error.")
|
||||
return
|
||||
}
|
||||
s.sendCodeResponse(w, r, authReq)
|
||||
return
|
||||
}
|
||||
|
||||
ui, err := s.storage.GetUserIdentity(ctx, authReq.Claims.UserID, authReq.ConnectorID)
|
||||
if err == nil && scopesCoveredByConsent(ui.Consents[authReq.ClientID], authReq.Scopes) {
|
||||
authReq, err := s.storage.GetAuthRequest(ctx, authReq.ID)
|
||||
if err != nil {
|
||||
s.logger.ErrorContext(ctx, "failed to get auth request", "err", err)
|
||||
s.renderError(r, w, http.StatusInternalServerError, "Login error.")
|
||||
return
|
||||
}
|
||||
s.sendCodeResponse(w, r, authReq)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
http.Redirect(w, r, approvalURL, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func generateTOTPQRCode(keyURL string) (string, error) {
|
||||
generated, err := otp.NewKeyFromURL(keyURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to load TOTP key: %w", err)
|
||||
}
|
||||
|
||||
qrCodeImage, err := generated.Image(300, 300)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate TOTP QR code: %w", err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, qrCodeImage); err != nil {
|
||||
return "", fmt.Errorf("failed to encode TOTP QR code: %w", err)
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(buf.Bytes()), nil
|
||||
}
|
||||
|
||||
// mfaChainForClient returns the MFA chain for a client filtered by connector type,
|
||||
// falling back to the server's defaultMFAChain if the client has none.
|
||||
// Returns nil if no MFA is configured/applicable.
|
||||
func (s *Server) mfaChainForClient(ctx context.Context, clientID, connectorID string) ([]string, error) {
|
||||
if len(s.mfaProviders) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
client, err := s.storage.GetClient(ctx, clientID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// nil means "not set" — fall back to default.
|
||||
// Explicit empty slice ([]string{}) means "no MFA" — don't fall back.
|
||||
source := client.MFAChain
|
||||
if source == nil {
|
||||
source = s.defaultMFAChain
|
||||
}
|
||||
|
||||
// Resolve connector type from connector ID.
|
||||
connectorType, err := s.getConnectorType(ctx, connectorID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var chain []string
|
||||
for _, authID := range source {
|
||||
provider, ok := s.mfaProviders[authID]
|
||||
if ok && provider.EnabledForConnectorType(connectorType) {
|
||||
chain = append(chain, authID)
|
||||
}
|
||||
}
|
||||
return chain, nil
|
||||
}
|
||||
|
||||
// getConnectorType returns the type of the connector with the given ID.
|
||||
func (s *Server) getConnectorType(ctx context.Context, connectorID string) (string, error) {
|
||||
conn, err := s.getConnector(ctx, connectorID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get connector %q: %w", connectorID, err)
|
||||
}
|
||||
return conn.Type, nil
|
||||
}
|
||||
@@ -57,6 +57,7 @@ const LocalConnector = "local"
|
||||
|
||||
// Connector is a connector with resource version metadata.
|
||||
type Connector struct {
|
||||
Type string
|
||||
ResourceVersion string
|
||||
Connector connector.Connector
|
||||
GrantTypes []string
|
||||
@@ -140,6 +141,12 @@ type Config struct {
|
||||
|
||||
// SessionConfig holds session settings. Nil when sessions are disabled.
|
||||
SessionConfig *SessionConfig
|
||||
|
||||
// MFAProviders maps authenticator IDs to their provider implementations.
|
||||
MFAProviders map[string]MFAProvider
|
||||
|
||||
// DefaultMFAChain is applied to clients that don't specify their own mfaChain.
|
||||
DefaultMFAChain []string
|
||||
}
|
||||
|
||||
// SessionConfig holds resolved session configuration.
|
||||
@@ -239,6 +246,9 @@ type Server struct {
|
||||
signer signer.Signer
|
||||
|
||||
sessionConfig *SessionConfig
|
||||
|
||||
mfaProviders map[string]MFAProvider
|
||||
defaultMFAChain []string
|
||||
}
|
||||
|
||||
// NewServer constructs a server from the provided config.
|
||||
@@ -363,6 +373,8 @@ func newServer(ctx context.Context, c Config) (*Server, error) {
|
||||
logger: c.Logger,
|
||||
signer: c.Signer,
|
||||
sessionConfig: c.SessionConfig,
|
||||
mfaProviders: c.MFAProviders,
|
||||
defaultMFAChain: c.DefaultMFAChain,
|
||||
}
|
||||
|
||||
// Retrieves connector objects in backend storage. This list includes the static connectors
|
||||
@@ -551,6 +563,7 @@ func newServer(ctx context.Context, c Config) (*Server, error) {
|
||||
// "authproxy" connector.
|
||||
handleFunc("/callback/{connector}", s.handleConnectorCallback)
|
||||
handleFunc("/approval", s.handleApproval)
|
||||
handleFunc("/mfa/verify", s.handleMFAVerify)
|
||||
handle("/healthz", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !c.HealthChecker.IsHealthy() {
|
||||
s.renderError(r, w, http.StatusInternalServerError, "Health check failed.")
|
||||
@@ -791,6 +804,7 @@ func (s *Server) OpenConnector(conn storage.Connector) (Connector, error) {
|
||||
}
|
||||
|
||||
connector := Connector{
|
||||
Type: conn.Type,
|
||||
ResourceVersion: conn.ResourceVersion,
|
||||
Connector: c,
|
||||
GrantTypes: conn.GrantTypes,
|
||||
|
||||
@@ -22,6 +22,7 @@ const (
|
||||
tmplError = "error.html"
|
||||
tmplDevice = "device.html"
|
||||
tmplDeviceSuccess = "device_success.html"
|
||||
tmplTOTPVerify = "totp_verify.html"
|
||||
)
|
||||
|
||||
var requiredTmpls = []string{
|
||||
@@ -42,6 +43,7 @@ type templates struct {
|
||||
errorTmpl *template.Template
|
||||
deviceTmpl *template.Template
|
||||
deviceSuccessTmpl *template.Template
|
||||
totpVerifyTmpl *template.Template
|
||||
}
|
||||
|
||||
type webConfig struct {
|
||||
@@ -169,6 +171,7 @@ func loadTemplates(c webConfig, templatesDir string) (*templates, error) {
|
||||
errorTmpl: tmpls.Lookup(tmplError),
|
||||
deviceTmpl: tmpls.Lookup(tmplDevice),
|
||||
deviceSuccessTmpl: tmpls.Lookup(tmplDeviceSuccess),
|
||||
totpVerifyTmpl: tmpls.Lookup(tmplTOTPVerify),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -338,6 +341,21 @@ func (t *templates) approval(r *http.Request, w http.ResponseWriter, authReqID,
|
||||
return renderTemplate(w, t.approvalTmpl, data)
|
||||
}
|
||||
|
||||
func (t *templates) totpVerify(r *http.Request, w http.ResponseWriter, postURL, issuer, connector, qrCode string, lastWasInvalid bool) error {
|
||||
if lastWasInvalid {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}
|
||||
data := struct {
|
||||
PostURL string
|
||||
Invalid bool
|
||||
Issuer string
|
||||
Connector string
|
||||
QRCode string
|
||||
ReqPath string
|
||||
}{postURL, lastWasInvalid, issuer, connector, qrCode, r.URL.Path}
|
||||
return renderTemplate(w, t.totpVerifyTmpl, data)
|
||||
}
|
||||
|
||||
func (t *templates) oob(r *http.Request, w http.ResponseWriter, code string) error {
|
||||
data := struct {
|
||||
Code string
|
||||
|
||||
@@ -32,6 +32,7 @@ func (d *Database) CreateAuthRequest(ctx context.Context, authRequest storage.Au
|
||||
SetConnectorID(authRequest.ConnectorID).
|
||||
SetConnectorData(authRequest.ConnectorData).
|
||||
SetHmacKey(authRequest.HMACKey).
|
||||
SetMfaValidated(authRequest.MFAValidated).
|
||||
Save(ctx)
|
||||
if err != nil {
|
||||
return convertDBError("create auth request: %w", err)
|
||||
@@ -96,6 +97,7 @@ func (d *Database) UpdateAuthRequest(ctx context.Context, id string, updater fun
|
||||
SetConnectorID(newAuthRequest.ConnectorID).
|
||||
SetConnectorData(newAuthRequest.ConnectorData).
|
||||
SetHmacKey(newAuthRequest.HMACKey).
|
||||
SetMfaValidated(newAuthRequest.MFAValidated).
|
||||
Save(context.TODO())
|
||||
if err != nil {
|
||||
return rollback(tx, "update auth request uploading: %w", err)
|
||||
|
||||
@@ -17,6 +17,7 @@ func (d *Database) CreateClient(ctx context.Context, client storage.Client) erro
|
||||
SetRedirectUris(client.RedirectURIs).
|
||||
SetTrustedPeers(client.TrustedPeers).
|
||||
SetAllowedConnectors(client.AllowedConnectors).
|
||||
SetMfaChain(client.MFAChain).
|
||||
Save(ctx)
|
||||
if err != nil {
|
||||
return convertDBError("create oauth2 client: %w", err)
|
||||
@@ -81,6 +82,7 @@ func (d *Database) UpdateClient(ctx context.Context, id string, updater func(old
|
||||
SetRedirectUris(newClient.RedirectURIs).
|
||||
SetTrustedPeers(newClient.TrustedPeers).
|
||||
SetAllowedConnectors(newClient.AllowedConnectors).
|
||||
SetMfaChain(newClient.MFAChain).
|
||||
Save(ctx)
|
||||
if err != nil {
|
||||
return rollback(tx, "update client uploading: %w", err)
|
||||
|
||||
@@ -45,7 +45,8 @@ func toStorageAuthRequest(a *db.AuthRequest) storage.AuthRequest {
|
||||
CodeChallenge: a.CodeChallenge,
|
||||
CodeChallengeMethod: a.CodeChallengeMethod,
|
||||
},
|
||||
HMACKey: a.HmacKey,
|
||||
HMACKey: a.HmacKey,
|
||||
MFAValidated: a.MfaValidated,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +85,7 @@ func toStorageClient(c *db.OAuth2Client) storage.Client {
|
||||
Name: c.Name,
|
||||
LogoURL: c.LogoURL,
|
||||
AllowedConnectors: c.AllowedConnectors,
|
||||
MFAChain: c.MfaChain,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,6 +195,18 @@ func toStorageUserIdentity(u *db.UserIdentity) storage.UserIdentity {
|
||||
// Server code assumes this will be non-nil.
|
||||
s.Consents = make(map[string][]string)
|
||||
}
|
||||
|
||||
if u.MfaSecrets != nil {
|
||||
if err := json.Unmarshal(*u.MfaSecrets, &s.MFASecrets); err != nil {
|
||||
// Correctness of json structure is guaranteed on uploading
|
||||
panic(err)
|
||||
}
|
||||
if s.MFASecrets == nil {
|
||||
s.MFASecrets = make(map[string]*storage.MFASecret)
|
||||
}
|
||||
} else {
|
||||
s.MFASecrets = make(map[string]*storage.MFASecret)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,14 @@ func (d *Database) CreateUserIdentity(ctx context.Context, identity storage.User
|
||||
return fmt.Errorf("encode consents user identity: %w", err)
|
||||
}
|
||||
|
||||
if identity.MFASecrets == nil {
|
||||
identity.MFASecrets = make(map[string]*storage.MFASecret)
|
||||
}
|
||||
encodedMFASecrets, err := json.Marshal(identity.MFASecrets)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode mfa secrets user identity: %w", err)
|
||||
}
|
||||
|
||||
id := compositeKeyID(identity.UserID, identity.ConnectorID, d.hasher)
|
||||
_, err = d.client.UserIdentity.Create().
|
||||
SetID(id).
|
||||
@@ -30,6 +38,7 @@ func (d *Database) CreateUserIdentity(ctx context.Context, identity storage.User
|
||||
SetClaimsEmailVerified(identity.Claims.EmailVerified).
|
||||
SetClaimsGroups(identity.Claims.Groups).
|
||||
SetConsents(encodedConsents).
|
||||
SetMfaSecrets(encodedMFASecrets).
|
||||
SetCreatedAt(identity.CreatedAt).
|
||||
SetLastLogin(identity.LastLogin).
|
||||
SetBlockedUntil(identity.BlockedUntil).
|
||||
@@ -90,6 +99,15 @@ func (d *Database) UpdateUserIdentity(ctx context.Context, userID string, connec
|
||||
return rollback(tx, "encode consents user identity: %w", err)
|
||||
}
|
||||
|
||||
if newUserIdentity.MFASecrets == nil {
|
||||
newUserIdentity.MFASecrets = make(map[string]*storage.MFASecret)
|
||||
}
|
||||
|
||||
encodedMFASecrets, err := json.Marshal(newUserIdentity.MFASecrets)
|
||||
if err != nil {
|
||||
return rollback(tx, "encode mfa secrets user identity: %w", err)
|
||||
}
|
||||
|
||||
_, err = tx.UserIdentity.UpdateOneID(id).
|
||||
SetUserID(newUserIdentity.UserID).
|
||||
SetConnectorID(newUserIdentity.ConnectorID).
|
||||
@@ -100,6 +118,7 @@ func (d *Database) UpdateUserIdentity(ctx context.Context, userID string, connec
|
||||
SetClaimsEmailVerified(newUserIdentity.Claims.EmailVerified).
|
||||
SetClaimsGroups(newUserIdentity.Claims.Groups).
|
||||
SetConsents(encodedConsents).
|
||||
SetMfaSecrets(encodedMFASecrets).
|
||||
SetCreatedAt(newUserIdentity.CreatedAt).
|
||||
SetLastLogin(newUserIdentity.LastLogin).
|
||||
SetBlockedUntil(newUserIdentity.BlockedUntil).
|
||||
|
||||
@@ -57,7 +57,9 @@ type AuthRequest struct {
|
||||
// CodeChallengeMethod holds the value of the "code_challenge_method" field.
|
||||
CodeChallengeMethod string `json:"code_challenge_method,omitempty"`
|
||||
// HmacKey holds the value of the "hmac_key" field.
|
||||
HmacKey []byte `json:"hmac_key,omitempty"`
|
||||
HmacKey []byte `json:"hmac_key,omitempty"`
|
||||
// MfaValidated holds the value of the "mfa_validated" field.
|
||||
MfaValidated bool `json:"mfa_validated,omitempty"`
|
||||
selectValues sql.SelectValues
|
||||
}
|
||||
|
||||
@@ -68,7 +70,7 @@ func (*AuthRequest) scanValues(columns []string) ([]any, error) {
|
||||
switch columns[i] {
|
||||
case authrequest.FieldScopes, authrequest.FieldResponseTypes, authrequest.FieldClaimsGroups, authrequest.FieldConnectorData, authrequest.FieldHmacKey:
|
||||
values[i] = new([]byte)
|
||||
case authrequest.FieldForceApprovalPrompt, authrequest.FieldLoggedIn, authrequest.FieldClaimsEmailVerified:
|
||||
case authrequest.FieldForceApprovalPrompt, authrequest.FieldLoggedIn, authrequest.FieldClaimsEmailVerified, authrequest.FieldMfaValidated:
|
||||
values[i] = new(sql.NullBool)
|
||||
case authrequest.FieldID, authrequest.FieldClientID, authrequest.FieldRedirectURI, authrequest.FieldNonce, authrequest.FieldState, authrequest.FieldClaimsUserID, authrequest.FieldClaimsUsername, authrequest.FieldClaimsEmail, authrequest.FieldClaimsPreferredUsername, authrequest.FieldConnectorID, authrequest.FieldCodeChallenge, authrequest.FieldCodeChallengeMethod:
|
||||
values[i] = new(sql.NullString)
|
||||
@@ -221,6 +223,12 @@ func (_m *AuthRequest) assignValues(columns []string, values []any) error {
|
||||
} else if value != nil {
|
||||
_m.HmacKey = *value
|
||||
}
|
||||
case authrequest.FieldMfaValidated:
|
||||
if value, ok := values[i].(*sql.NullBool); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field mfa_validated", values[i])
|
||||
} else if value.Valid {
|
||||
_m.MfaValidated = value.Bool
|
||||
}
|
||||
default:
|
||||
_m.selectValues.Set(columns[i], values[i])
|
||||
}
|
||||
@@ -318,6 +326,9 @@ func (_m *AuthRequest) String() string {
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("hmac_key=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.HmacKey))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("mfa_validated=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.MfaValidated))
|
||||
builder.WriteByte(')')
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
@@ -51,6 +51,8 @@ const (
|
||||
FieldCodeChallengeMethod = "code_challenge_method"
|
||||
// FieldHmacKey holds the string denoting the hmac_key field in the database.
|
||||
FieldHmacKey = "hmac_key"
|
||||
// FieldMfaValidated holds the string denoting the mfa_validated field in the database.
|
||||
FieldMfaValidated = "mfa_validated"
|
||||
// Table holds the table name of the authrequest in the database.
|
||||
Table = "auth_requests"
|
||||
)
|
||||
@@ -78,6 +80,7 @@ var Columns = []string{
|
||||
FieldCodeChallenge,
|
||||
FieldCodeChallengeMethod,
|
||||
FieldHmacKey,
|
||||
FieldMfaValidated,
|
||||
}
|
||||
|
||||
// ValidColumn reports if the column name is valid (part of the table columns).
|
||||
@@ -97,6 +100,8 @@ var (
|
||||
DefaultCodeChallenge string
|
||||
// DefaultCodeChallengeMethod holds the default value on creation for the "code_challenge_method" field.
|
||||
DefaultCodeChallengeMethod string
|
||||
// DefaultMfaValidated holds the default value on creation for the "mfa_validated" field.
|
||||
DefaultMfaValidated bool
|
||||
// IDValidator is a validator for the "id" field. It is called by the builders before save.
|
||||
IDValidator func(string) error
|
||||
)
|
||||
@@ -183,3 +188,8 @@ func ByCodeChallenge(opts ...sql.OrderTermOption) OrderOption {
|
||||
func ByCodeChallengeMethod(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldCodeChallengeMethod, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByMfaValidated orders the results by the mfa_validated field.
|
||||
func ByMfaValidated(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldMfaValidated, opts...).ToFunc()
|
||||
}
|
||||
|
||||
@@ -149,6 +149,11 @@ func HmacKey(v []byte) predicate.AuthRequest {
|
||||
return predicate.AuthRequest(sql.FieldEQ(FieldHmacKey, v))
|
||||
}
|
||||
|
||||
// MfaValidated applies equality check predicate on the "mfa_validated" field. It's identical to MfaValidatedEQ.
|
||||
func MfaValidated(v bool) predicate.AuthRequest {
|
||||
return predicate.AuthRequest(sql.FieldEQ(FieldMfaValidated, v))
|
||||
}
|
||||
|
||||
// ClientIDEQ applies the EQ predicate on the "client_id" field.
|
||||
func ClientIDEQ(v string) predicate.AuthRequest {
|
||||
return predicate.AuthRequest(sql.FieldEQ(FieldClientID, v))
|
||||
@@ -1054,6 +1059,16 @@ func HmacKeyLTE(v []byte) predicate.AuthRequest {
|
||||
return predicate.AuthRequest(sql.FieldLTE(FieldHmacKey, v))
|
||||
}
|
||||
|
||||
// MfaValidatedEQ applies the EQ predicate on the "mfa_validated" field.
|
||||
func MfaValidatedEQ(v bool) predicate.AuthRequest {
|
||||
return predicate.AuthRequest(sql.FieldEQ(FieldMfaValidated, v))
|
||||
}
|
||||
|
||||
// MfaValidatedNEQ applies the NEQ predicate on the "mfa_validated" field.
|
||||
func MfaValidatedNEQ(v bool) predicate.AuthRequest {
|
||||
return predicate.AuthRequest(sql.FieldNEQ(FieldMfaValidated, v))
|
||||
}
|
||||
|
||||
// And groups predicates with the AND operator between them.
|
||||
func And(predicates ...predicate.AuthRequest) predicate.AuthRequest {
|
||||
return predicate.AuthRequest(sql.AndPredicates(predicates...))
|
||||
|
||||
@@ -164,6 +164,20 @@ func (_c *AuthRequestCreate) SetHmacKey(v []byte) *AuthRequestCreate {
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetMfaValidated sets the "mfa_validated" field.
|
||||
func (_c *AuthRequestCreate) SetMfaValidated(v bool) *AuthRequestCreate {
|
||||
_c.mutation.SetMfaValidated(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableMfaValidated sets the "mfa_validated" field if the given value is not nil.
|
||||
func (_c *AuthRequestCreate) SetNillableMfaValidated(v *bool) *AuthRequestCreate {
|
||||
if v != nil {
|
||||
_c.SetMfaValidated(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetID sets the "id" field.
|
||||
func (_c *AuthRequestCreate) SetID(v string) *AuthRequestCreate {
|
||||
_c.mutation.SetID(v)
|
||||
@@ -217,6 +231,10 @@ func (_c *AuthRequestCreate) defaults() {
|
||||
v := authrequest.DefaultCodeChallengeMethod
|
||||
_c.mutation.SetCodeChallengeMethod(v)
|
||||
}
|
||||
if _, ok := _c.mutation.MfaValidated(); !ok {
|
||||
v := authrequest.DefaultMfaValidated
|
||||
_c.mutation.SetMfaValidated(v)
|
||||
}
|
||||
}
|
||||
|
||||
// check runs all checks and user-defined validators on the builder.
|
||||
@@ -269,6 +287,9 @@ func (_c *AuthRequestCreate) check() error {
|
||||
if _, ok := _c.mutation.HmacKey(); !ok {
|
||||
return &ValidationError{Name: "hmac_key", err: errors.New(`db: missing required field "AuthRequest.hmac_key"`)}
|
||||
}
|
||||
if _, ok := _c.mutation.MfaValidated(); !ok {
|
||||
return &ValidationError{Name: "mfa_validated", err: errors.New(`db: missing required field "AuthRequest.mfa_validated"`)}
|
||||
}
|
||||
if v, ok := _c.mutation.ID(); ok {
|
||||
if err := authrequest.IDValidator(v); err != nil {
|
||||
return &ValidationError{Name: "id", err: fmt.Errorf(`db: validator failed for field "AuthRequest.id": %w`, err)}
|
||||
@@ -389,6 +410,10 @@ func (_c *AuthRequestCreate) createSpec() (*AuthRequest, *sqlgraph.CreateSpec) {
|
||||
_spec.SetField(authrequest.FieldHmacKey, field.TypeBytes, value)
|
||||
_node.HmacKey = value
|
||||
}
|
||||
if value, ok := _c.mutation.MfaValidated(); ok {
|
||||
_spec.SetField(authrequest.FieldMfaValidated, field.TypeBool, value)
|
||||
_node.MfaValidated = value
|
||||
}
|
||||
return _node, _spec
|
||||
}
|
||||
|
||||
|
||||
@@ -311,6 +311,20 @@ func (_u *AuthRequestUpdate) SetHmacKey(v []byte) *AuthRequestUpdate {
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetMfaValidated sets the "mfa_validated" field.
|
||||
func (_u *AuthRequestUpdate) SetMfaValidated(v bool) *AuthRequestUpdate {
|
||||
_u.mutation.SetMfaValidated(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableMfaValidated sets the "mfa_validated" field if the given value is not nil.
|
||||
func (_u *AuthRequestUpdate) SetNillableMfaValidated(v *bool) *AuthRequestUpdate {
|
||||
if v != nil {
|
||||
_u.SetMfaValidated(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// Mutation returns the AuthRequestMutation object of the builder.
|
||||
func (_u *AuthRequestUpdate) Mutation() *AuthRequestMutation {
|
||||
return _u.mutation
|
||||
@@ -439,6 +453,9 @@ func (_u *AuthRequestUpdate) sqlSave(ctx context.Context) (_node int, err error)
|
||||
if value, ok := _u.mutation.HmacKey(); ok {
|
||||
_spec.SetField(authrequest.FieldHmacKey, field.TypeBytes, value)
|
||||
}
|
||||
if value, ok := _u.mutation.MfaValidated(); ok {
|
||||
_spec.SetField(authrequest.FieldMfaValidated, field.TypeBool, value)
|
||||
}
|
||||
if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil {
|
||||
if _, ok := err.(*sqlgraph.NotFoundError); ok {
|
||||
err = &NotFoundError{authrequest.Label}
|
||||
@@ -741,6 +758,20 @@ func (_u *AuthRequestUpdateOne) SetHmacKey(v []byte) *AuthRequestUpdateOne {
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetMfaValidated sets the "mfa_validated" field.
|
||||
func (_u *AuthRequestUpdateOne) SetMfaValidated(v bool) *AuthRequestUpdateOne {
|
||||
_u.mutation.SetMfaValidated(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableMfaValidated sets the "mfa_validated" field if the given value is not nil.
|
||||
func (_u *AuthRequestUpdateOne) SetNillableMfaValidated(v *bool) *AuthRequestUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetMfaValidated(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// Mutation returns the AuthRequestMutation object of the builder.
|
||||
func (_u *AuthRequestUpdateOne) Mutation() *AuthRequestMutation {
|
||||
return _u.mutation
|
||||
@@ -899,6 +930,9 @@ func (_u *AuthRequestUpdateOne) sqlSave(ctx context.Context) (_node *AuthRequest
|
||||
if value, ok := _u.mutation.HmacKey(); ok {
|
||||
_spec.SetField(authrequest.FieldHmacKey, field.TypeBytes, value)
|
||||
}
|
||||
if value, ok := _u.mutation.MfaValidated(); ok {
|
||||
_spec.SetField(authrequest.FieldMfaValidated, field.TypeBool, value)
|
||||
}
|
||||
_node = &AuthRequest{config: _u.config}
|
||||
_spec.Assign = _node.assignValues
|
||||
_spec.ScanValues = _node.scanValues
|
||||
|
||||
@@ -56,6 +56,7 @@ var (
|
||||
{Name: "code_challenge", Type: field.TypeString, Size: 2147483647, Default: "", SchemaType: map[string]string{"mysql": "varchar(384)", "postgres": "text", "sqlite3": "text"}},
|
||||
{Name: "code_challenge_method", Type: field.TypeString, Size: 2147483647, Default: "", SchemaType: map[string]string{"mysql": "varchar(384)", "postgres": "text", "sqlite3": "text"}},
|
||||
{Name: "hmac_key", Type: field.TypeBytes},
|
||||
{Name: "mfa_validated", Type: field.TypeBool, Default: false},
|
||||
}
|
||||
// AuthRequestsTable holds the schema information for the "auth_requests" table.
|
||||
AuthRequestsTable = &schema.Table{
|
||||
@@ -154,6 +155,7 @@ var (
|
||||
{Name: "name", Type: field.TypeString, Size: 2147483647, SchemaType: map[string]string{"mysql": "varchar(384)", "postgres": "text", "sqlite3": "text"}},
|
||||
{Name: "logo_url", Type: field.TypeString, Size: 2147483647, SchemaType: map[string]string{"mysql": "varchar(384)", "postgres": "text", "sqlite3": "text"}},
|
||||
{Name: "allowed_connectors", Type: field.TypeJSON, Nullable: true},
|
||||
{Name: "mfa_chain", Type: field.TypeJSON, Nullable: true},
|
||||
}
|
||||
// Oauth2clientsTable holds the schema information for the "oauth2clients" table.
|
||||
Oauth2clientsTable = &schema.Table{
|
||||
@@ -230,6 +232,7 @@ var (
|
||||
{Name: "claims_email_verified", Type: field.TypeBool, Default: false},
|
||||
{Name: "claims_groups", Type: field.TypeJSON, Nullable: true},
|
||||
{Name: "consents", Type: field.TypeBytes},
|
||||
{Name: "mfa_secrets", Type: field.TypeBytes, Nullable: true},
|
||||
{Name: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"mysql": "datetime(3)", "postgres": "timestamptz", "sqlite3": "timestamp"}},
|
||||
{Name: "last_login", Type: field.TypeTime, SchemaType: map[string]string{"mysql": "datetime(3)", "postgres": "timestamptz", "sqlite3": "timestamp"}},
|
||||
{Name: "blocked_until", Type: field.TypeTime, SchemaType: map[string]string{"mysql": "datetime(3)", "postgres": "timestamptz", "sqlite3": "timestamp"}},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user