mirror of
https://github.com/netbirdio/dex.git
synced 2026-05-22 18:43:53 -07:00
feat: add auth_time, prompt, and max_age fields (#4662)
Signed-off-by: maksim.nabokikh <max.nabokih@gmail.com>
This commit is contained in:
@@ -6,3 +6,4 @@
|
||||
/docker-compose.override.yaml
|
||||
/var/
|
||||
/vendor/
|
||||
*.db
|
||||
|
||||
+42
-18
@@ -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)
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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, " ")
|
||||
}
|
||||
@@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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...))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user