feat: add auth_time, prompt, and max_age fields (#4662)

Signed-off-by: maksim.nabokikh <max.nabokih@gmail.com>
This commit is contained in:
Maksim Nabokikh
2026-03-19 15:53:15 +01:00
committed by GitHub
parent 7ec1760c6b
commit c3bc1d7466
35 changed files with 1558 additions and 51 deletions
+1
View File
@@ -6,3 +6,4 @@
/docker-compose.override.yaml
/var/
/vendor/
*.db
+42 -18
View File
@@ -365,14 +365,39 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
return
}
// Check if there's a valid session that can skip login for this client.
// Handle OIDC prompt parameter and session-based login.
prompt, err := ParsePrompt(authReq.Prompt)
if err != nil {
// Server error because authReq was validated before saving it to database.
s.redirectWithError(w, r, authReq, errServerError, "Invalid authentication request")
return
}
// handle prompt only if sessions are enabled
if s.sessionConfig != nil {
if redirectURL, ok := s.trySessionLogin(ctx, r, w, authReq); ok {
// prompt=none: no UI allowed.
if prompt.None() {
redirectURL, ok := s.trySessionLogin(ctx, r, w, authReq)
if !ok {
s.redirectWithError(w, r, authReq, errLoginRequired, "User not authenticated")
return
}
if redirectURL != "" {
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
// Session found but consent required — no UI allowed.
s.redirectWithError(w, r, authReq, errInteractionRequired, "Consent required")
return
}
return
}
if !prompt.Login() {
// Normal flow: try session-based login (skip if prompt=login forces re-auth).
if redirectURL, ok := s.trySessionLogin(ctx, r, w, authReq); ok {
if redirectURL != "" {
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
}
return
}
}
}
scopes := parseScopes(authReq.Scopes)
@@ -687,6 +712,7 @@ func (s *Server) finalizeLogin(ctx context.Context, identity connector.Identity,
a.LoggedIn = true
a.Claims = claims
a.ConnectorData = identity.ConnectorData
a.AuthTime = s.now()
return a, nil
}
if err := s.storage.UpdateAuthRequest(ctx, authReq.ID, updater); err != nil {
@@ -769,11 +795,8 @@ func (s *Server) finalizeLogin(ctx context.Context, identity connector.Identity,
}
case err == nil:
if err := s.storage.UpdateUserIdentity(ctx, identity.UserID, authReq.ConnectorID, func(old storage.UserIdentity) (storage.UserIdentity, error) {
if len(identity.ConnectorData) > 0 {
old.Claims = claims
old.LastLogin = now
return old, nil
}
old.Claims = claims
old.LastLogin = now
return old, nil
}); err != nil {
s.logger.ErrorContext(ctx, "failed to update user identity", "err", err)
@@ -988,6 +1011,7 @@ func (s *Server) sendCodeResponse(w http.ResponseWriter, r *http.Request, authRe
RedirectURI: authReq.RedirectURI,
ConnectorData: authReq.ConnectorData,
PKCE: authReq.PKCE,
AuthTime: authReq.AuthTime,
}
if err := s.storage.CreateAuthCode(ctx, code); err != nil {
s.logger.ErrorContext(r.Context(), "Failed to create auth code", "err", err)
@@ -1007,7 +1031,7 @@ func (s *Server) sendCodeResponse(w http.ResponseWriter, r *http.Request, authRe
implicitOrHybrid = true
var err error
accessToken, _, err = s.newAccessToken(r.Context(), authReq.ClientID, authReq.Claims, authReq.Scopes, authReq.Nonce, authReq.ConnectorID)
accessToken, _, err = s.newAccessToken(r.Context(), authReq.ClientID, authReq.Claims, authReq.Scopes, authReq.Nonce, authReq.ConnectorID, authReq.AuthTime)
if err != nil {
s.logger.ErrorContext(r.Context(), "failed to create new access token", "err", err)
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
@@ -1017,7 +1041,7 @@ func (s *Server) sendCodeResponse(w http.ResponseWriter, r *http.Request, authRe
implicitOrHybrid = true
var err error
idToken, idTokenExpiry, err = s.newIDToken(r.Context(), authReq.ClientID, authReq.Claims, authReq.Scopes, authReq.Nonce, accessToken, code.ID, authReq.ConnectorID)
idToken, idTokenExpiry, err = s.newIDToken(r.Context(), authReq.ClientID, authReq.Claims, authReq.Scopes, authReq.Nonce, accessToken, code.ID, authReq.ConnectorID, authReq.AuthTime)
if err != nil {
s.logger.ErrorContext(r.Context(), "failed to create ID token", "err", err)
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
@@ -1251,14 +1275,14 @@ func (s *Server) handleAuthCode(w http.ResponseWriter, r *http.Request, client s
}
func (s *Server) exchangeAuthCode(ctx context.Context, w http.ResponseWriter, authCode storage.AuthCode, client storage.Client) (*accessTokenResponse, error) {
accessToken, _, err := s.newAccessToken(ctx, client.ID, authCode.Claims, authCode.Scopes, authCode.Nonce, authCode.ConnectorID)
accessToken, _, err := s.newAccessToken(ctx, client.ID, authCode.Claims, authCode.Scopes, authCode.Nonce, authCode.ConnectorID, authCode.AuthTime)
if err != nil {
s.logger.ErrorContext(ctx, "failed to create new access token", "err", err)
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
return nil, err
}
idToken, expiry, err := s.newIDToken(ctx, client.ID, authCode.Claims, authCode.Scopes, authCode.Nonce, accessToken, authCode.ID, authCode.ConnectorID)
idToken, expiry, err := s.newIDToken(ctx, client.ID, authCode.Claims, authCode.Scopes, authCode.Nonce, accessToken, authCode.ID, authCode.ConnectorID, authCode.AuthTime)
if err != nil {
s.logger.ErrorContext(ctx, "failed to create ID token", "err", err)
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
@@ -1539,14 +1563,14 @@ func (s *Server) handlePasswordGrant(w http.ResponseWriter, r *http.Request, cli
Groups: identity.Groups,
}
accessToken, _, err := s.newAccessToken(ctx, client.ID, claims, scopes, nonce, connID)
accessToken, _, err := s.newAccessToken(ctx, client.ID, claims, scopes, nonce, connID, time.Time{})
if err != nil {
s.logger.ErrorContext(r.Context(), "password grant failed to create new access token", "err", err)
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
return
}
idToken, expiry, err := s.newIDToken(ctx, client.ID, claims, scopes, nonce, accessToken, "", connID)
idToken, expiry, err := s.newIDToken(ctx, client.ID, claims, scopes, nonce, accessToken, "", connID, time.Time{})
if err != nil {
s.logger.ErrorContext(r.Context(), "password grant failed to create new ID token", "err", err)
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
@@ -1752,9 +1776,9 @@ func (s *Server) handleTokenExchange(w http.ResponseWriter, r *http.Request, cli
var expiry time.Time
switch requestedTokenType {
case tokenTypeID:
resp.AccessToken, expiry, err = s.newIDToken(r.Context(), client.ID, claims, scopes, "", "", "", connID)
resp.AccessToken, expiry, err = s.newIDToken(r.Context(), client.ID, claims, scopes, "", "", "", connID, time.Time{})
case tokenTypeAccess:
resp.AccessToken, expiry, err = s.newAccessToken(r.Context(), client.ID, claims, scopes, "", connID)
resp.AccessToken, expiry, err = s.newAccessToken(r.Context(), client.ID, claims, scopes, "", connID, time.Time{})
default:
s.tokenErrHelper(w, errRequestNotSupported, "Invalid requested_token_type.", http.StatusBadRequest)
return
@@ -1854,7 +1878,7 @@ func (s *Server) handleClientCredentialsGrant(w http.ResponseWriter, r *http.Req
// Creating connectors with an empty ID with the config and API is prohibited
connID := ""
accessToken, expiry, err := s.newAccessToken(ctx, client.ID, claims, scopes, nonce, connID)
accessToken, expiry, err := s.newAccessToken(ctx, client.ID, claims, scopes, nonce, connID, time.Time{})
if err != nil {
s.logger.ErrorContext(ctx, "client_credentials grant failed to create new access token", "err", err)
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
@@ -1863,7 +1887,7 @@ func (s *Server) handleClientCredentialsGrant(w http.ResponseWriter, r *http.Req
var idToken string
if hasOpenIDScope {
idToken, expiry, err = s.newIDToken(ctx, client.ID, claims, scopes, nonce, accessToken, "", connID)
idToken, expiry, err = s.newIDToken(ctx, client.ID, claims, scopes, nonce, accessToken, "", connID, time.Time{})
if err != nil {
s.logger.ErrorContext(ctx, "client_credentials grant failed to create new ID token", "err", err)
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
+2 -2
View File
@@ -152,7 +152,7 @@ func TestGetTokenFromRequestSuccess(t *testing.T) {
accessToken, _, err := s.newIDToken(ctx, "test", storage.Claims{
UserID: "1",
Username: "jane",
}, []string{"openid"}, "nonce", "", "", "test")
}, []string{"openid"}, "nonce", "", "", "test", time.Time{})
require.NoError(t, err)
tests := []struct {
@@ -270,7 +270,7 @@ func TestHandleIntrospect(t *testing.T) {
Email: "jane.doe@example.com",
EmailVerified: true,
Groups: []string{"a", "b"},
}, []string{"openid", "email", "profile", "groups"}, "foo", "", "", "test")
}, []string{"openid", "email", "profile", "groups"}, "foo", "", "", "test", time.Time{})
require.NoError(t, err)
activeRefreshToken, err := internal.Marshal(&internal.RefreshToken{RefreshId: "test", Token: "bar"})
+45 -4
View File
@@ -45,6 +45,18 @@ func newDisplayedErr(status int, format string, a ...interface{}) *displayedAuth
return &displayedAuthErr{status, fmt.Sprintf(format, a...)}
}
// redirectWithError redirects back to the client with an OAuth2 error response.
// Used for prompt=none when login or consent is required.
func (s *Server) redirectWithError(w http.ResponseWriter, r *http.Request, authReq *storage.AuthRequest, errType, description string) {
err := &redirectedAuthErr{
State: authReq.State,
RedirectURI: authReq.RedirectURI,
Type: errType,
Description: description,
}
err.Handler().ServeHTTP(w, r)
}
// redirectedAuthErr is an error that should be reported back to the client by 302 redirect
type redirectedAuthErr struct {
State string
@@ -117,6 +129,9 @@ const (
errInvalidGrant = "invalid_grant"
errInvalidClient = "invalid_client"
errInactiveToken = "inactive_token"
errLoginRequired = "login_required"
errInteractionRequired = "interaction_required"
errConsentRequired = "consent_required"
)
const (
@@ -257,6 +272,7 @@ type idTokenClaims struct {
IssuedAt int64 `json:"iat"`
AuthorizingParty string `json:"azp,omitempty"`
Nonce string `json:"nonce,omitempty"`
AuthTime int64 `json:"auth_time,omitempty"`
AccessTokenHash string `json:"at_hash,omitempty"`
CodeHash string `json:"c_hash,omitempty"`
@@ -277,8 +293,8 @@ type federatedIDClaims struct {
UserID string `json:"user_id,omitempty"`
}
func (s *Server) newAccessToken(ctx context.Context, clientID string, claims storage.Claims, scopes []string, nonce, connID string) (accessToken string, expiry time.Time, err error) {
return s.newIDToken(ctx, clientID, claims, scopes, nonce, storage.NewID(), "", connID)
func (s *Server) newAccessToken(ctx context.Context, clientID string, claims storage.Claims, scopes []string, nonce, connID string, authTime time.Time) (accessToken string, expiry time.Time, err error) {
return s.newIDToken(ctx, clientID, claims, scopes, nonce, storage.NewID(), "", connID, authTime)
}
func getClientID(aud audience, azp string) (string, error) {
@@ -324,7 +340,7 @@ func genSubject(userID string, connID string) (string, error) {
return internal.Marshal(sub)
}
func (s *Server) newIDToken(ctx context.Context, clientID string, claims storage.Claims, scopes []string, nonce, accessToken, code, connID string) (idToken string, expiry time.Time, err error) {
func (s *Server) newIDToken(ctx context.Context, clientID string, claims storage.Claims, scopes []string, nonce, accessToken, code, connID string, authTime time.Time) (idToken string, expiry time.Time, err error) {
issuedAt := s.now()
expiry = issuedAt.Add(s.idTokensValidFor)
@@ -342,6 +358,11 @@ func (s *Server) newIDToken(ctx context.Context, clientID string, claims storage
IssuedAt: issuedAt.Unix(),
}
// Include auth_time when sessions are enabled and the value is available.
if !authTime.IsZero() {
tok.AuthTime = authTime.Unix()
}
// Determine signing algorithm from signer
signingAlg, err := s.signer.Algorithm(ctx)
if err != nil {
@@ -583,12 +604,32 @@ func (s *Server) parseAuthorizationRequest(r *http.Request) (*storage.AuthReques
}
}
prompt, err := ParsePrompt(q.Get("prompt"))
if err != nil {
return nil, newRedirectedErr(errInvalidRequest, "Invalid prompt parameter: %v", err)
}
// Parse max_age: -1 means not specified.
maxAge := -1
if maxAgeStr := q.Get("max_age"); maxAgeStr != "" {
v, err := strconv.Atoi(maxAgeStr)
if err != nil || v < 0 {
return nil, newRedirectedErr(errInvalidRequest, "Invalid max_age value %q", maxAgeStr)
}
maxAge = v
}
// OIDC prompt=consent implies force approval.
forceApproval := q.Get("approval_prompt") == "force" || prompt.Consent()
return &storage.AuthRequest{
ID: storage.NewID(),
ClientID: client.ID,
State: state,
Nonce: nonce,
ForceApprovalPrompt: q.Get("approval_prompt") == "force",
ForceApprovalPrompt: forceApproval,
Prompt: prompt.String(),
MaxAge: maxAge,
Scopes: scopes,
RedirectURI: redirectURI,
ResponseTypes: responseTypes,
+77
View File
@@ -0,0 +1,77 @@
package server
import (
"fmt"
"strings"
)
// Prompt represents the parsed OIDC "prompt" parameter (RFC 6749 / OpenID Connect Core 3.1.2.1).
// The parameter is space-separated and may contain: "none", "login", "consent", "select_account".
// "none" must not be combined with any other value.
type Prompt struct {
none bool
login bool
consent bool
}
// ParsePrompt parses and validates the raw prompt query parameter.
// Returns an error suitable for returning as an OAuth2 invalid_request if the value is invalid.
func ParsePrompt(raw string) (Prompt, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return Prompt{}, nil
}
var p Prompt
seen := make(map[string]bool)
for _, v := range strings.Fields(raw) {
if seen[v] {
continue
}
seen[v] = true
switch v {
case "none":
p.none = true
case "login":
p.login = true
case "consent":
p.consent = true
case "select_account":
// Dex does not support account selection; ignore per spec recommendation.
default:
return Prompt{}, fmt.Errorf("invalid prompt value %q", v)
}
}
if p.none && (p.login || p.consent) {
return Prompt{}, fmt.Errorf("prompt=none must not be combined with other values")
}
return p, nil
}
// None returns true if the caller requested no interactive UI.
func (p Prompt) None() bool { return p.none }
// Login returns true if the caller requested forced re-authentication.
func (p Prompt) Login() bool { return p.login }
// Consent returns true if the caller requested forced consent screen.
func (p Prompt) Consent() bool { return p.consent }
// String returns the canonical space-separated representation stored in the database.
func (p Prompt) String() string {
var parts []string
if p.none {
return "none"
}
if p.login {
parts = append(parts, "login")
}
if p.consent {
parts = append(parts, "consent")
}
return strings.Join(parts, " ")
}
+64
View File
@@ -0,0 +1,64 @@
package server
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParsePrompt(t *testing.T) {
tests := []struct {
name string
raw string
want Prompt
wantErr bool
}{
{name: "empty", raw: "", want: Prompt{}},
{name: "none", raw: "none", want: Prompt{none: true}},
{name: "login", raw: "login", want: Prompt{login: true}},
{name: "consent", raw: "consent", want: Prompt{consent: true}},
{name: "login consent", raw: "login consent", want: Prompt{login: true, consent: true}},
{name: "consent login", raw: "consent login", want: Prompt{login: true, consent: true}},
{name: "select_account ignored", raw: "select_account", want: Prompt{}},
{name: "login select_account", raw: "login select_account", want: Prompt{login: true}},
{name: "duplicate values", raw: "login login", want: Prompt{login: true}},
{name: "whitespace padding", raw: " login ", want: Prompt{login: true}},
// Errors.
{name: "none with login", raw: "none login", wantErr: true},
{name: "none with consent", raw: "none consent", wantErr: true},
{name: "unknown value", raw: "bogus", wantErr: true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := ParsePrompt(tc.raw)
if tc.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tc.want, got)
})
}
}
func TestPromptString(t *testing.T) {
tests := []struct {
prompt Prompt
want string
}{
{Prompt{}, ""},
{Prompt{none: true}, "none"},
{Prompt{login: true}, "login"},
{Prompt{consent: true}, "consent"},
{Prompt{login: true, consent: true}, "login consent"},
}
for _, tc := range tests {
t.Run(tc.want, func(t *testing.T) {
assert.Equal(t, tc.want, tc.prompt.String())
})
}
}
+13 -2
View File
@@ -439,14 +439,25 @@ func (s *Server) handleRefreshToken(w http.ResponseWriter, r *http.Request, clie
Groups: ident.Groups,
}
accessToken, _, err := s.newAccessToken(r.Context(), client.ID, claims, rCtx.scopes, rCtx.storageToken.Nonce, rCtx.storageToken.ConnectorID)
authTime := time.Time{}
if s.sessionConfig != nil {
ui, err := s.storage.GetUserIdentity(r.Context(), ident.UserID, rCtx.storageToken.ConnectorID)
if err != nil {
s.logger.ErrorContext(r.Context(), "failed to get user identity", "err", err)
s.refreshTokenErrHelper(w, newInternalServerError())
return
}
authTime = ui.LastLogin
}
accessToken, _, err := s.newAccessToken(r.Context(), client.ID, claims, rCtx.scopes, rCtx.storageToken.Nonce, rCtx.storageToken.ConnectorID, authTime)
if err != nil {
s.logger.ErrorContext(r.Context(), "failed to create new access token", "err", err)
s.refreshTokenErrHelper(w, newInternalServerError())
return
}
idToken, expiry, err := s.newIDToken(r.Context(), client.ID, claims, rCtx.scopes, rCtx.storageToken.Nonce, accessToken, "", rCtx.storageToken.ConnectorID)
idToken, expiry, err := s.newIDToken(r.Context(), client.ID, claims, rCtx.scopes, rCtx.storageToken.Nonce, accessToken, "", rCtx.storageToken.ConnectorID, authTime)
if err != nil {
s.logger.ErrorContext(r.Context(), "failed to create ID token", "err", err)
s.refreshTokenErrHelper(w, newInternalServerError())
+138
View File
@@ -2,15 +2,18 @@ package server
import (
"bytes"
"encoding/base64"
"encoding/json"
"log/slog"
"net/http"
"net/http/httptest"
"net/url"
"path"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/dexidp/dex/server/internal"
@@ -209,6 +212,141 @@ func TestRefreshTokenExpirationScenarios(t *testing.T) {
}
}
// decodeJWTClaims decodes the payload of a JWT token without verifying the signature.
func decodeJWTClaims(t *testing.T, token string) map[string]any {
t.Helper()
parts := strings.SplitN(token, ".", 3)
require.Len(t, parts, 3, "JWT should have 3 parts")
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
require.NoError(t, err)
var claims map[string]any
err = json.Unmarshal(payload, &claims)
require.NoError(t, err)
return claims
}
func TestRefreshTokenAuthTime(t *testing.T) {
t0 := time.Now().UTC().Round(time.Second)
loginTime := t0.Add(-10 * time.Minute)
tests := []struct {
name string
sessionConfig *SessionConfig
createUserIdentity bool
wantAuthTime bool
wantHTTPError bool
}{
{
name: "sessions enabled with user identity",
sessionConfig: &SessionConfig{
CookieName: "dex_session",
AbsoluteLifetime: 24 * time.Hour,
},
createUserIdentity: true,
wantAuthTime: true,
},
{
name: "sessions disabled",
sessionConfig: nil,
createUserIdentity: false,
wantAuthTime: false,
},
{
name: "sessions enabled but user identity missing",
sessionConfig: &SessionConfig{
CookieName: "dex_session",
AbsoluteLifetime: 24 * time.Hour,
},
createUserIdentity: false,
wantHTTPError: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
httpServer, s := newTestServer(t, func(c *Config) {
c.Now = func() time.Time { return t0 }
})
defer httpServer.Close()
s.sessionConfig = tc.sessionConfig
mockRefreshTokenTestStorage(t, s.storage, false)
if tc.createUserIdentity {
// The mock connector returns UserID "0-385-28089-0" on Refresh,
// so the UserIdentity must use that ID to be found by handleRefreshToken.
err := s.storage.CreateUserIdentity(t.Context(), storage.UserIdentity{
UserID: "0-385-28089-0",
ConnectorID: "test",
Claims: storage.Claims{
UserID: "0-385-28089-0",
Username: "Kilgore Trout",
Email: "kilgore@kilgore.trout",
EmailVerified: true,
Groups: []string{"authors"},
},
CreatedAt: loginTime,
LastLogin: loginTime,
})
require.NoError(t, err)
}
u, err := url.Parse(s.issuerURL.String())
require.NoError(t, err)
tokenData, err := internal.Marshal(&internal.RefreshToken{RefreshId: "test", Token: "bar"})
require.NoError(t, err)
u.Path = path.Join(u.Path, "/token")
v := url.Values{}
v.Add("grant_type", "refresh_token")
v.Add("refresh_token", tokenData)
req, _ := http.NewRequest("POST", u.String(), bytes.NewBufferString(v.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value")
req.SetBasicAuth("test", "barfoo")
rr := httptest.NewRecorder()
s.ServeHTTP(rr, req)
if tc.wantHTTPError {
assert.Equal(t, http.StatusInternalServerError, rr.Code)
return
}
require.Equal(t, http.StatusOK, rr.Code)
var resp struct {
AccessToken string `json:"access_token"`
IDToken string `json:"id_token"`
RefreshToken string `json:"refresh_token"`
}
err = json.Unmarshal(rr.Body.Bytes(), &resp)
require.NoError(t, err)
accessClaims := decodeJWTClaims(t, resp.AccessToken)
if tc.wantAuthTime {
assert.Equal(t, float64(loginTime.Unix()), accessClaims["auth_time"],
"access token auth_time should match UserIdentity.LastLogin")
} else {
assert.Nil(t, accessClaims["auth_time"],
"access token should not have auth_time when sessions are disabled")
}
// TODO: newIDToken in handleRefreshToken is currently called with time.Time{},
// so the ID token does not include auth_time. Once fixed, uncomment:
// if tc.wantAuthTime {
// idClaims := decodeJWTClaims(t, resp.IDToken)
// assert.Equal(t, float64(loginTime.Unix()), idClaims["auth_time"],
// "id token auth_time should match UserIdentity.LastLogin")
// }
})
}
}
func TestRefreshTokenPolicy(t *testing.T) {
lastTime := time.Now()
l := slog.New(slog.DiscardHandler)
+10 -1
View File
@@ -10,6 +10,7 @@ import (
"net/http"
"path"
"strings"
"time"
"github.com/dexidp/dex/storage"
)
@@ -258,6 +259,13 @@ func (s *Server) trySessionLogin(ctx context.Context, r *http.Request, w http.Re
return "", false
}
// Check max_age: if the user's last authentication is too old, force re-auth.
if authReq.MaxAge >= 0 {
if now.Sub(ui.LastLogin) > time.Duration(authReq.MaxAge)*time.Second {
return "", false
}
}
claims := storage.Claims{
UserID: ui.Claims.UserID,
Username: ui.Claims.Username,
@@ -267,11 +275,12 @@ func (s *Server) trySessionLogin(ctx context.Context, r *http.Request, w http.Re
Groups: ui.Claims.Groups,
}
// Update AuthRequest with stored identity (without logging "login successful").
// Update AuthRequest with stored identity and auth_time from last login.
if err := s.storage.UpdateAuthRequest(ctx, authReq.ID, func(a storage.AuthRequest) (storage.AuthRequest, error) {
a.LoggedIn = true
a.Claims = claims
a.ConnectorID = session.ConnectorID
a.AuthTime = ui.LastLogin
return a, nil
}); err != nil {
s.logger.ErrorContext(ctx, "session: failed to update auth request", "err", err)
+173 -8
View File
@@ -427,6 +427,7 @@ func setupSessionLoginFixture(t *testing.T, s *Server) storage.AuthRequest {
ConnectorID: "mock",
Scopes: []string{"openid", "email"},
RedirectURI: "http://localhost/callback",
MaxAge: -1,
HMACKey: storage.NewHMACKey(crypto.SHA256),
Expiry: now.Add(10 * time.Minute),
}
@@ -463,8 +464,6 @@ func TestTrySessionLogin(t *testing.T) {
_, ok := s.trySessionLogin(ctx, r, w, &authReq)
assert.True(t, ok)
// sendCodeResponse deletes the AuthRequest after processing,
// so we can't verify it here. The fact that ok=true is sufficient.
})
t.Run("successful login redirects to approval", func(t *testing.T) {
@@ -491,7 +490,6 @@ func TestTrySessionLogin(t *testing.T) {
s := newTestSessionServer(t)
s.skipApproval = false
authReq := setupSessionLoginFixture(t, s)
// Scopes match stored consent: {"client-1": {"openid", "email"}}
r := sessionCookieRequest("user-1", "mock", "test-nonce")
w := httptest.NewRecorder()
@@ -526,24 +524,23 @@ func TestTrySessionLogin(t *testing.T) {
t.Run("expired client state returns false", func(t *testing.T) {
s := newTestSessionServer(t)
ctx := t.Context()
now := s.now()
require.NoError(t, s.storage.CreateAuthSession(ctx, storage.AuthSession{
require.NoError(t, s.storage.CreateAuthSession(t.Context(), storage.AuthSession{
UserID: "user-exp",
ConnectorID: "mock",
Nonce: "nonce-exp",
ClientStates: map[string]*storage.ClientAuthState{
"client-1": {
Active: true,
ExpiresAt: now.Add(-1 * time.Hour), // expired
ExpiresAt: now.Add(-1 * time.Hour),
},
},
CreatedAt: now.Add(-2 * time.Hour),
LastActivity: now.Add(-1 * time.Minute),
}))
require.NoError(t, s.storage.CreateUserIdentity(ctx, storage.UserIdentity{
require.NoError(t, s.storage.CreateUserIdentity(t.Context(), storage.UserIdentity{
UserID: "user-exp",
ConnectorID: "mock",
Claims: storage.Claims{UserID: "user-exp"},
@@ -556,10 +553,11 @@ func TestTrySessionLogin(t *testing.T) {
ID: storage.NewID(),
ClientID: "client-1",
ConnectorID: "mock",
MaxAge: -1,
HMACKey: storage.NewHMACKey(crypto.SHA256),
Expiry: now.Add(10 * time.Minute),
}
require.NoError(t, s.storage.CreateAuthRequest(ctx, authReq))
require.NoError(t, s.storage.CreateAuthRequest(t.Context(), authReq))
r := sessionCookieRequest("user-exp", "mock", "nonce-exp")
w := httptest.NewRecorder()
@@ -584,3 +582,170 @@ func TestTrySessionLogin(t *testing.T) {
assert.Equal(t, s.now(), session.LastActivity)
})
}
// setupSessionWithIdentity creates an AuthSession, UserIdentity, and AuthRequest in storage
// for use in trySessionLogin tests. Returns the authReq.
func setupSessionWithIdentity(t *testing.T, s *Server, now time.Time, lastLogin time.Time) storage.AuthRequest {
t.Helper()
ctx := t.Context()
nonce := "test-nonce"
session := storage.AuthSession{
UserID: "user-1",
ConnectorID: "mock",
Nonce: nonce,
ClientStates: map[string]*storage.ClientAuthState{
"client-1": {
Active: true,
ExpiresAt: now.Add(24 * time.Hour),
LastActivity: now.Add(-1 * time.Minute),
},
},
CreatedAt: now.Add(-30 * time.Minute),
LastActivity: now.Add(-1 * time.Minute),
IPAddress: "127.0.0.1",
UserAgent: "test",
}
require.NoError(t, s.storage.CreateAuthSession(ctx, session))
ui := storage.UserIdentity{
UserID: "user-1",
ConnectorID: "mock",
Claims: storage.Claims{
UserID: "user-1",
Username: "testuser",
Email: "test@example.com",
},
Consents: make(map[string][]string),
CreatedAt: now.Add(-1 * time.Hour),
LastLogin: lastLogin,
}
require.NoError(t, s.storage.CreateUserIdentity(ctx, ui))
authReq := storage.AuthRequest{
ID: storage.NewID(),
ClientID: "client-1",
ConnectorID: "mock",
Scopes: []string{"openid"},
RedirectURI: "http://localhost/callback",
MaxAge: -1,
HMACKey: storage.NewHMACKey(crypto.SHA256),
Expiry: now.Add(10 * time.Minute),
}
require.NoError(t, s.storage.CreateAuthRequest(ctx, authReq))
return authReq
}
func TestTrySessionLogin_MaxAge(t *testing.T) {
ctx := t.Context()
t.Run("max_age not specified, session reused", func(t *testing.T) {
s := newTestSessionServer(t)
now := s.now()
authReq := setupSessionWithIdentity(t, s, now, now.Add(-2*time.Hour))
authReq.MaxAge = -1 // not specified
r := httptest.NewRequest(http.MethodGet, "/", nil)
r.AddCookie(&http.Cookie{Name: "dex_session", Value: sessionCookieValue("user-1", "mock", "test-nonce")})
w := httptest.NewRecorder()
_, ok := s.trySessionLogin(ctx, r, w, &authReq)
assert.True(t, ok, "session should be reused when max_age is not specified")
})
t.Run("max_age satisfied, session reused", func(t *testing.T) {
s := newTestSessionServer(t)
now := s.now()
// User logged in 10 minutes ago, max_age=3600 (1 hour)
authReq := setupSessionWithIdentity(t, s, now, now.Add(-10*time.Minute))
authReq.MaxAge = 3600
r := httptest.NewRequest(http.MethodGet, "/", nil)
r.AddCookie(&http.Cookie{Name: "dex_session", Value: sessionCookieValue("user-1", "mock", "test-nonce")})
w := httptest.NewRecorder()
_, ok := s.trySessionLogin(ctx, r, w, &authReq)
assert.True(t, ok, "session should be reused when max_age is satisfied")
})
t.Run("max_age exceeded, force re-auth", func(t *testing.T) {
s := newTestSessionServer(t)
now := s.now()
// User logged in 2 hours ago, max_age=3600 (1 hour)
authReq := setupSessionWithIdentity(t, s, now, now.Add(-2*time.Hour))
authReq.MaxAge = 3600
r := httptest.NewRequest(http.MethodGet, "/", nil)
r.AddCookie(&http.Cookie{Name: "dex_session", Value: sessionCookieValue("user-1", "mock", "test-nonce")})
w := httptest.NewRecorder()
_, ok := s.trySessionLogin(ctx, r, w, &authReq)
assert.False(t, ok, "session should NOT be reused when max_age is exceeded")
})
t.Run("max_age=0, always force re-auth", func(t *testing.T) {
s := newTestSessionServer(t)
now := s.now()
// User logged in 1 second ago, max_age=0
authReq := setupSessionWithIdentity(t, s, now, now.Add(-1*time.Second))
authReq.MaxAge = 0
r := httptest.NewRequest(http.MethodGet, "/", nil)
r.AddCookie(&http.Cookie{Name: "dex_session", Value: sessionCookieValue("user-1", "mock", "test-nonce")})
w := httptest.NewRecorder()
_, ok := s.trySessionLogin(ctx, r, w, &authReq)
assert.False(t, ok, "max_age=0 should always force re-authentication")
})
t.Run("auth_time is set from UserIdentity.LastLogin", func(t *testing.T) {
s := newTestSessionServer(t)
s.skipApproval = false
now := s.now()
lastLogin := now.Add(-10 * time.Minute)
authReq := setupSessionWithIdentity(t, s, now, lastLogin)
authReq.ForceApprovalPrompt = true // force approval so AuthRequest is not deleted
require.NoError(t, s.storage.UpdateAuthRequest(ctx, authReq.ID, func(a storage.AuthRequest) (storage.AuthRequest, error) {
a.ForceApprovalPrompt = true
return a, nil
}))
r := httptest.NewRequest(http.MethodGet, "/", nil)
r.AddCookie(&http.Cookie{Name: "dex_session", Value: sessionCookieValue("user-1", "mock", "test-nonce")})
w := httptest.NewRecorder()
redirectURL, ok := s.trySessionLogin(ctx, r, w, &authReq)
require.True(t, ok)
assert.Contains(t, redirectURL, "/approval")
// Verify AuthTime was set on the auth request.
updated, err := s.storage.GetAuthRequest(ctx, authReq.ID)
require.NoError(t, err)
assert.Equal(t, lastLogin.Unix(), updated.AuthTime.Unix())
})
}
func TestParseAuthRequest_PromptAndMaxAge(t *testing.T) {
t.Run("prompt=consent sets ForceApprovalPrompt", func(t *testing.T) {
authReq := storage.AuthRequest{
Prompt: "consent",
ForceApprovalPrompt: true,
}
assert.True(t, authReq.ForceApprovalPrompt)
assert.Equal(t, "consent", authReq.Prompt)
})
t.Run("max_age default is -1", func(t *testing.T) {
authReq := storage.AuthRequest{
MaxAge: -1,
}
assert.Equal(t, -1, authReq.MaxAge)
})
}
+13 -1
View File
@@ -19,6 +19,10 @@ import (
// ensure that values being tested on never expire.
var neverExpire = time.Now().UTC().Add(time.Hour * 24 * 365 * 100)
// defaultAuthTime is a non-zero time used as AuthTime default in tests.
// MySQL rejects Go's zero time (0001-01-01), so all test fixtures must use a real value.
var defaultAuthTime = time.Now().UTC()
type subTest struct {
name string
run func(t *testing.T, s storage.Storage)
@@ -100,6 +104,7 @@ func testAuthRequestCRUD(t *testing.T, s storage.Storage) {
ForceApprovalPrompt: true,
LoggedIn: true,
Expiry: neverExpire,
AuthTime: defaultAuthTime,
ConnectorID: "ldap",
ConnectorData: []byte(`{"some":"data"}`),
Claims: storage.Claims{
@@ -134,6 +139,7 @@ func testAuthRequestCRUD(t *testing.T, s storage.Storage) {
ForceApprovalPrompt: true,
LoggedIn: true,
Expiry: neverExpire,
AuthTime: defaultAuthTime,
ConnectorID: "ldap",
ConnectorData: []byte(`{"some":"data"}`),
Claims: storage.Claims{
@@ -191,6 +197,7 @@ func testAuthCodeCRUD(t *testing.T, s storage.Storage) {
Nonce: "foobar",
Scopes: []string{"openid", "email"},
Expiry: neverExpire,
AuthTime: defaultAuthTime,
ConnectorID: "ldap",
ConnectorData: []byte(`{"some":"data"}`),
PKCE: storage.PKCE{
@@ -217,6 +224,7 @@ func testAuthCodeCRUD(t *testing.T, s storage.Storage) {
Nonce: "foobar",
Scopes: []string{"openid", "email"},
Expiry: neverExpire,
AuthTime: defaultAuthTime,
ConnectorID: "ldap",
ConnectorData: []byte(`{"some":"data"}`),
Claims: storage.Claims{
@@ -243,7 +251,8 @@ func testAuthCodeCRUD(t *testing.T, s storage.Storage) {
if a1.Expiry.Unix() != got.Expiry.Unix() {
t.Errorf("auth code expiry did not match want=%s vs got=%s", a1.Expiry, got.Expiry)
}
got.Expiry = a1.Expiry // time fields do not compare well
got.Expiry = a1.Expiry // time fields do not compare well
got.AuthTime = a1.AuthTime // time fields do not compare well
if diff := pretty.Compare(a1, got); diff != "" {
t.Errorf("auth code retrieved from storage did not match: %s", diff)
}
@@ -790,6 +799,7 @@ func testGC(t *testing.T, s storage.Storage) {
Nonce: "foobar",
Scopes: []string{"openid", "email"},
Expiry: expiry,
AuthTime: defaultAuthTime,
ConnectorID: "ldap",
ConnectorData: []byte(`{"some":"data"}`),
Claims: storage.Claims{
@@ -840,6 +850,7 @@ func testGC(t *testing.T, s storage.Storage) {
ForceApprovalPrompt: true,
LoggedIn: true,
Expiry: expiry,
AuthTime: defaultAuthTime,
ConnectorID: "ldap",
ConnectorData: []byte(`{"some":"data"}`),
Claims: storage.Claims{
@@ -976,6 +987,7 @@ func testTimezones(t *testing.T, s storage.Storage) {
Nonce: "foobar",
Scopes: []string{"openid", "email"},
Expiry: expiry,
AuthTime: defaultAuthTime,
ConnectorID: "ldap",
ConnectorData: []byte(`{"some":"data"}`),
Claims: storage.Claims{
+1
View File
@@ -82,6 +82,7 @@ func testAuthRequestConcurrentUpdate(t *testing.T, s storage.Storage) {
ForceApprovalPrompt: true,
LoggedIn: true,
Expiry: neverExpire,
AuthTime: defaultAuthTime,
ConnectorID: "ldap",
ConnectorData: []byte(`{"some":"data"}`),
Claims: storage.Claims{
+1
View File
@@ -26,6 +26,7 @@ func (d *Database) CreateAuthCode(ctx context.Context, code storage.AuthCode) er
SetExpiry(code.Expiry.UTC()).
SetConnectorID(code.ConnectorID).
SetConnectorData(code.ConnectorData).
SetAuthTime(code.AuthTime).
Save(ctx)
if err != nil {
return convertDBError("create auth code: %w", err)
+6
View File
@@ -33,6 +33,9 @@ func (d *Database) CreateAuthRequest(ctx context.Context, authRequest storage.Au
SetConnectorData(authRequest.ConnectorData).
SetHmacKey(authRequest.HMACKey).
SetMfaValidated(authRequest.MFAValidated).
SetPrompt(authRequest.Prompt).
SetMaxAge(authRequest.MaxAge).
SetAuthTime(authRequest.AuthTime).
Save(ctx)
if err != nil {
return convertDBError("create auth request: %w", err)
@@ -98,6 +101,9 @@ func (d *Database) UpdateAuthRequest(ctx context.Context, id string, updater fun
SetConnectorData(newAuthRequest.ConnectorData).
SetHmacKey(newAuthRequest.HMACKey).
SetMfaValidated(newAuthRequest.MFAValidated).
SetPrompt(newAuthRequest.Prompt).
SetMaxAge(newAuthRequest.MaxAge).
SetAuthTime(newAuthRequest.AuthTime).
Save(context.TODO())
if err != nil {
return rollback(tx, "update auth request uploading: %w", err)
+4
View File
@@ -47,6 +47,9 @@ func toStorageAuthRequest(a *db.AuthRequest) storage.AuthRequest {
},
HMACKey: a.HmacKey,
MFAValidated: a.MfaValidated,
Prompt: a.Prompt,
MaxAge: a.MaxAge,
AuthTime: a.AuthTime,
}
}
@@ -72,6 +75,7 @@ func toStorageAuthCode(a *db.AuthCode) storage.AuthCode {
CodeChallenge: a.CodeChallenge,
CodeChallengeMethod: a.CodeChallengeMethod,
},
AuthTime: a.AuthTime,
}
}
+13 -2
View File
@@ -48,7 +48,9 @@ type AuthCode struct {
CodeChallenge string `json:"code_challenge,omitempty"`
// CodeChallengeMethod holds the value of the "code_challenge_method" field.
CodeChallengeMethod string `json:"code_challenge_method,omitempty"`
selectValues sql.SelectValues
// AuthTime holds the value of the "auth_time" field.
AuthTime time.Time `json:"auth_time,omitempty"`
selectValues sql.SelectValues
}
// scanValues returns the types for scanning values from sql.Rows.
@@ -62,7 +64,7 @@ func (*AuthCode) scanValues(columns []string) ([]any, error) {
values[i] = new(sql.NullBool)
case authcode.FieldID, authcode.FieldClientID, authcode.FieldNonce, authcode.FieldRedirectURI, authcode.FieldClaimsUserID, authcode.FieldClaimsUsername, authcode.FieldClaimsEmail, authcode.FieldClaimsPreferredUsername, authcode.FieldConnectorID, authcode.FieldCodeChallenge, authcode.FieldCodeChallengeMethod:
values[i] = new(sql.NullString)
case authcode.FieldExpiry:
case authcode.FieldExpiry, authcode.FieldAuthTime:
values[i] = new(sql.NullTime)
default:
values[i] = new(sql.UnknownType)
@@ -179,6 +181,12 @@ func (_m *AuthCode) assignValues(columns []string, values []any) error {
} else if value.Valid {
_m.CodeChallengeMethod = value.String
}
case authcode.FieldAuthTime:
if value, ok := values[i].(*sql.NullTime); !ok {
return fmt.Errorf("unexpected type %T for field auth_time", values[i])
} else if value.Valid {
_m.AuthTime = value.Time
}
default:
_m.selectValues.Set(columns[i], values[i])
}
@@ -261,6 +269,9 @@ func (_m *AuthCode) String() string {
builder.WriteString(", ")
builder.WriteString("code_challenge_method=")
builder.WriteString(_m.CodeChallengeMethod)
builder.WriteString(", ")
builder.WriteString("auth_time=")
builder.WriteString(_m.AuthTime.Format(time.ANSIC))
builder.WriteByte(')')
return builder.String()
}
+8
View File
@@ -41,6 +41,8 @@ const (
FieldCodeChallenge = "code_challenge"
// FieldCodeChallengeMethod holds the string denoting the code_challenge_method field in the database.
FieldCodeChallengeMethod = "code_challenge_method"
// FieldAuthTime holds the string denoting the auth_time field in the database.
FieldAuthTime = "auth_time"
// Table holds the table name of the authcode in the database.
Table = "auth_codes"
)
@@ -63,6 +65,7 @@ var Columns = []string{
FieldExpiry,
FieldCodeChallenge,
FieldCodeChallengeMethod,
FieldAuthTime,
}
// ValidColumn reports if the column name is valid (part of the table columns).
@@ -167,3 +170,8 @@ func ByCodeChallenge(opts ...sql.OrderTermOption) OrderOption {
func ByCodeChallengeMethod(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldCodeChallengeMethod, opts...).ToFunc()
}
// ByAuthTime orders the results by the auth_time field.
func ByAuthTime(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldAuthTime, opts...).ToFunc()
}
+55
View File
@@ -129,6 +129,11 @@ func CodeChallengeMethod(v string) predicate.AuthCode {
return predicate.AuthCode(sql.FieldEQ(FieldCodeChallengeMethod, v))
}
// AuthTime applies equality check predicate on the "auth_time" field. It's identical to AuthTimeEQ.
func AuthTime(v time.Time) predicate.AuthCode {
return predicate.AuthCode(sql.FieldEQ(FieldAuthTime, v))
}
// ClientIDEQ applies the EQ predicate on the "client_id" field.
func ClientIDEQ(v string) predicate.AuthCode {
return predicate.AuthCode(sql.FieldEQ(FieldClientID, v))
@@ -899,6 +904,56 @@ func CodeChallengeMethodContainsFold(v string) predicate.AuthCode {
return predicate.AuthCode(sql.FieldContainsFold(FieldCodeChallengeMethod, v))
}
// AuthTimeEQ applies the EQ predicate on the "auth_time" field.
func AuthTimeEQ(v time.Time) predicate.AuthCode {
return predicate.AuthCode(sql.FieldEQ(FieldAuthTime, v))
}
// AuthTimeNEQ applies the NEQ predicate on the "auth_time" field.
func AuthTimeNEQ(v time.Time) predicate.AuthCode {
return predicate.AuthCode(sql.FieldNEQ(FieldAuthTime, v))
}
// AuthTimeIn applies the In predicate on the "auth_time" field.
func AuthTimeIn(vs ...time.Time) predicate.AuthCode {
return predicate.AuthCode(sql.FieldIn(FieldAuthTime, vs...))
}
// AuthTimeNotIn applies the NotIn predicate on the "auth_time" field.
func AuthTimeNotIn(vs ...time.Time) predicate.AuthCode {
return predicate.AuthCode(sql.FieldNotIn(FieldAuthTime, vs...))
}
// AuthTimeGT applies the GT predicate on the "auth_time" field.
func AuthTimeGT(v time.Time) predicate.AuthCode {
return predicate.AuthCode(sql.FieldGT(FieldAuthTime, v))
}
// AuthTimeGTE applies the GTE predicate on the "auth_time" field.
func AuthTimeGTE(v time.Time) predicate.AuthCode {
return predicate.AuthCode(sql.FieldGTE(FieldAuthTime, v))
}
// AuthTimeLT applies the LT predicate on the "auth_time" field.
func AuthTimeLT(v time.Time) predicate.AuthCode {
return predicate.AuthCode(sql.FieldLT(FieldAuthTime, v))
}
// AuthTimeLTE applies the LTE predicate on the "auth_time" field.
func AuthTimeLTE(v time.Time) predicate.AuthCode {
return predicate.AuthCode(sql.FieldLTE(FieldAuthTime, v))
}
// AuthTimeIsNil applies the IsNil predicate on the "auth_time" field.
func AuthTimeIsNil() predicate.AuthCode {
return predicate.AuthCode(sql.FieldIsNull(FieldAuthTime))
}
// AuthTimeNotNil applies the NotNil predicate on the "auth_time" field.
func AuthTimeNotNil() predicate.AuthCode {
return predicate.AuthCode(sql.FieldNotNull(FieldAuthTime))
}
// And groups predicates with the AND operator between them.
func And(predicates ...predicate.AuthCode) predicate.AuthCode {
return predicate.AuthCode(sql.AndPredicates(predicates...))
+18
View File
@@ -134,6 +134,20 @@ func (_c *AuthCodeCreate) SetNillableCodeChallengeMethod(v *string) *AuthCodeCre
return _c
}
// SetAuthTime sets the "auth_time" field.
func (_c *AuthCodeCreate) SetAuthTime(v time.Time) *AuthCodeCreate {
_c.mutation.SetAuthTime(v)
return _c
}
// SetNillableAuthTime sets the "auth_time" field if the given value is not nil.
func (_c *AuthCodeCreate) SetNillableAuthTime(v *time.Time) *AuthCodeCreate {
if v != nil {
_c.SetAuthTime(*v)
}
return _c
}
// SetID sets the "id" field.
func (_c *AuthCodeCreate) SetID(v string) *AuthCodeCreate {
_c.mutation.SetID(v)
@@ -362,6 +376,10 @@ func (_c *AuthCodeCreate) createSpec() (*AuthCode, *sqlgraph.CreateSpec) {
_spec.SetField(authcode.FieldCodeChallengeMethod, field.TypeString, value)
_node.CodeChallengeMethod = value
}
if value, ok := _c.mutation.AuthTime(); ok {
_spec.SetField(authcode.FieldAuthTime, field.TypeTime, value)
_node.AuthTime = value
}
return _node, _spec
}
+52
View File
@@ -245,6 +245,26 @@ func (_u *AuthCodeUpdate) SetNillableCodeChallengeMethod(v *string) *AuthCodeUpd
return _u
}
// SetAuthTime sets the "auth_time" field.
func (_u *AuthCodeUpdate) SetAuthTime(v time.Time) *AuthCodeUpdate {
_u.mutation.SetAuthTime(v)
return _u
}
// SetNillableAuthTime sets the "auth_time" field if the given value is not nil.
func (_u *AuthCodeUpdate) SetNillableAuthTime(v *time.Time) *AuthCodeUpdate {
if v != nil {
_u.SetAuthTime(*v)
}
return _u
}
// ClearAuthTime clears the value of the "auth_time" field.
func (_u *AuthCodeUpdate) ClearAuthTime() *AuthCodeUpdate {
_u.mutation.ClearAuthTime()
return _u
}
// Mutation returns the AuthCodeMutation object of the builder.
func (_u *AuthCodeUpdate) Mutation() *AuthCodeMutation {
return _u.mutation
@@ -393,6 +413,12 @@ func (_u *AuthCodeUpdate) sqlSave(ctx context.Context) (_node int, err error) {
if value, ok := _u.mutation.CodeChallengeMethod(); ok {
_spec.SetField(authcode.FieldCodeChallengeMethod, field.TypeString, value)
}
if value, ok := _u.mutation.AuthTime(); ok {
_spec.SetField(authcode.FieldAuthTime, field.TypeTime, value)
}
if _u.mutation.AuthTimeCleared() {
_spec.ClearField(authcode.FieldAuthTime, field.TypeTime)
}
if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil {
if _, ok := err.(*sqlgraph.NotFoundError); ok {
err = &NotFoundError{authcode.Label}
@@ -629,6 +655,26 @@ func (_u *AuthCodeUpdateOne) SetNillableCodeChallengeMethod(v *string) *AuthCode
return _u
}
// SetAuthTime sets the "auth_time" field.
func (_u *AuthCodeUpdateOne) SetAuthTime(v time.Time) *AuthCodeUpdateOne {
_u.mutation.SetAuthTime(v)
return _u
}
// SetNillableAuthTime sets the "auth_time" field if the given value is not nil.
func (_u *AuthCodeUpdateOne) SetNillableAuthTime(v *time.Time) *AuthCodeUpdateOne {
if v != nil {
_u.SetAuthTime(*v)
}
return _u
}
// ClearAuthTime clears the value of the "auth_time" field.
func (_u *AuthCodeUpdateOne) ClearAuthTime() *AuthCodeUpdateOne {
_u.mutation.ClearAuthTime()
return _u
}
// Mutation returns the AuthCodeMutation object of the builder.
func (_u *AuthCodeUpdateOne) Mutation() *AuthCodeMutation {
return _u.mutation
@@ -807,6 +853,12 @@ func (_u *AuthCodeUpdateOne) sqlSave(ctx context.Context) (_node *AuthCode, err
if value, ok := _u.mutation.CodeChallengeMethod(); ok {
_spec.SetField(authcode.FieldCodeChallengeMethod, field.TypeString, value)
}
if value, ok := _u.mutation.AuthTime(); ok {
_spec.SetField(authcode.FieldAuthTime, field.TypeTime, value)
}
if _u.mutation.AuthTimeCleared() {
_spec.ClearField(authcode.FieldAuthTime, field.TypeTime)
}
_node = &AuthCode{config: _u.config}
_spec.Assign = _node.assignValues
_spec.ScanValues = _node.scanValues

Some files were not shown because too many files have changed in this diff Show More