Skip to content

Commit 27ba0ea

Browse files
committed
Add signed JWT auto sign-in flow (LibreGraph.SignedLoginOK)
When --allow-client-signed-logins is enabled, trusted apps can sign in users without an interactive password challenge by sending an OIDC authorization request that includes the LibreGraph.SignedLoginOK scope together with a signed request object carrying a preferred_username claim. The signed login path is built directly into IdentifierIdentityManager: - checkAndRecordJTI: in-memory JTI replay prevention (10 min window) - authenticateSignedLogin: validates the signed JWT, looks up the user by preferred_username, writes a logon cookie so subsequent silent-renew and re-auth requests succeed via the normal cookie path without needing a new signed JWT each time - authorizeSignedLogin: verifies client identity against the signed request object and approves scopes via the client's trusted_scopes list WriteLogonCookie is introduced on Identifier to write the cookie mid-flow without firing onSetLogonCallbacks (which would produce a spurious browser-state cookie header in the same response). SetUserToLogonCookie is refactored to delegate to it.
1 parent e9dd6ff commit 27ba0ea

13 files changed

Lines changed: 232 additions & 24 deletions

File tree

bootstrap/backends/ldap/ldap.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,13 +160,18 @@ func NewIdentityManager(bs bootstrap.Bootstrap) (identity.Manager, error) {
160160
return nil, fmt.Errorf("invalid --encryption-secret parameter value for identifier: %v", err)
161161
}
162162

163+
// Expose the identifier in the managers registry so other managers (e.g.
164+
// signedlogin) can access it without creating a second instance.
165+
bs.Managers().Set("identifier", activeIdentifier)
166+
163167
identityManagerConfig := &identity.Config{
164168
SignInFormURI: fullSignInFormURL,
165169
SignedOutURI: fullSignedOutEndpointURL,
166170

167171
Logger: logger,
168172

169-
ScopesSupported: config.Config.AllowedScopes,
173+
ScopesSupported: config.Config.AllowedScopes,
174+
AllowSignedLogin: config.Config.AllowClientSignedLogins,
170175
}
171176

172177
identifierIdentityManager := managers.NewIdentifierIdentityManager(identityManagerConfig, activeIdentifier)

bootstrap/backends/libregraph/libregraph.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,13 +143,18 @@ func NewIdentityManager(bs bootstrap.Bootstrap) (identity.Manager, error) {
143143
return nil, fmt.Errorf("invalid --encryption-secret parameter value for identifier: %v", err)
144144
}
145145

146+
// Expose the identifier in the managers registry so other managers (e.g.
147+
// signedlogin) can access it without creating a second instance.
148+
bs.Managers().Set("identifier", activeIdentifier)
149+
146150
identityManagerConfig := &identity.Config{
147151
SignInFormURI: fullSignInFormURL,
148152
SignedOutURI: fullSignedOutEndpointURL,
149153

150154
Logger: logger,
151155

152-
ScopesSupported: config.Config.AllowedScopes,
156+
ScopesSupported: config.Config.AllowedScopes,
157+
AllowSignedLogin: config.Config.AllowClientSignedLogins,
153158
}
154159

155160
identifierIdentityManager := managers.NewIdentifierIdentityManager(identityManagerConfig, activeIdentifier)

bootstrap/bootstrap.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,11 @@ func (bs *bootstrap) initialize(settings *Settings) error {
206206
logger.Infoln("client controlled guests are enabled")
207207
}
208208

209+
bs.config.Config.AllowClientSignedLogins = settings.AllowClientSignedLogins
210+
if bs.config.Config.AllowClientSignedLogins {
211+
logger.Infoln("client controlled signed logins are enabled")
212+
}
213+
209214
bs.config.Config.AllowDynamicClientRegistration = settings.AllowDynamicClientRegistration
210215
if bs.config.Config.AllowDynamicClientRegistration {
211216
logger.Infoln("dynamic client registration is enabled")

bootstrap/settings.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type Settings struct {
3535
TrustedProxy []string
3636
AllowScope []string
3737
AllowClientGuests bool
38+
AllowClientSignedLogins bool
3839
AllowDynamicClientRegistration bool
3940
EncryptionSecretFile string
4041
Listen string

cmd/licod/serve.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ func commandServe() *cobra.Command {
9898
serveCmd.Flags().StringArrayVar(&cfg.TrustedProxy, "trusted-proxy", nil, "Trusted proxy IP or IP network (can be used multiple times)")
9999
serveCmd.Flags().StringArrayVar(&cfg.AllowScope, "allow-scope", nil, "Allow OAuth 2 scope (can be used multiple times, if not set default scopes are allowed)")
100100
serveCmd.Flags().BoolVar(&cfg.AllowClientGuests, "allow-client-guests", false, "Allow sign in of client controlled guest users")
101+
serveCmd.Flags().BoolVar(&cfg.AllowClientSignedLogins, "allow-client-signed-logins", false, "Allow sign in of client controlled signed login users")
101102
serveCmd.Flags().BoolVar(&cfg.AllowDynamicClientRegistration, "allow-dynamic-client-registration", false, "Allow dynamic OAuth2 client registration")
102103
serveCmd.Flags().Uint64Var(&cfg.AccessTokenDurationSeconds, "access-token-expiration", 60*10, "Expiration time of access tokens in seconds since generated") // 10 Minutes.
103104
serveCmd.Flags().Uint64Var(&cfg.IDTokenDurationSeconds, "id-token-expiration", 60*60, "Expiration time of id tokens in seconds since generated") // 1 Hour.

config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,6 @@ type Config struct {
3838

3939
AllowedScopes []string
4040
AllowClientGuests bool
41+
AllowClientSignedLogins bool
4142
AllowDynamicClientRegistration bool
4243
}

identifier/identifier.go

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -269,27 +269,26 @@ func (i *Identifier) ErrorPage(rw http.ResponseWriter, code int, title string, m
269269
utils.WriteErrorPage(rw, code, title, message)
270270
}
271271

272-
// SetUserToLogonCookie serializes the provided user into an encrypted string
273-
// and sets it as cookie on the provided http.ResponseWriter.
274-
func (i *Identifier) SetUserToLogonCookie(ctx context.Context, rw http.ResponseWriter, user *IdentifiedUser) error {
272+
// WriteLogonCookie serializes the provided user into an encrypted string and
273+
// sets it as a logon cookie without firing any callbacks. Use this when the
274+
// cookie must be written mid-flow (e.g. inside an authorize handler) where
275+
// the caller is responsible for updating browser state separately.
276+
func (i *Identifier) WriteLogonCookie(rw http.ResponseWriter, user *IdentifiedUser) error {
275277
loggedOn, logonAt := user.LoggedOn()
276278
if !loggedOn {
277279
return fmt.Errorf("refused to set cookie for not logged on user")
278280
}
279281

280-
// Add standard claims.
281282
claims := jwt.Claims{
282283
Issuer: user.BackendName(),
283284
Audience: audienceMarker,
284285
Subject: user.Subject(),
285286
IssuedAt: jwt.NewNumericDate(logonAt),
286287
}
287-
// Add expiration, if set.
288288
if user.expiresAfter != nil {
289289
claims.Expiry = jwt.NewNumericDate(*user.expiresAfter)
290290
}
291291

292-
// Additional claims.
293292
userClaims := map[string]interface{}(user.Claims())
294293
if sessionRef := user.SessionRef(); sessionRef != nil {
295294
userClaims[SessionIDClaim] = *sessionRef
@@ -304,25 +303,27 @@ func (i *Identifier) SetUserToLogonCookie(ctx context.Context, rw http.ResponseW
304303
userClaims[LockedScopesClaim] = strings.Join(lockedScopes, " ")
305304
}
306305

307-
// Serialize and encrypt cookie value.
308306
serialized, err := jwt.Encrypted(i.encrypter).Claims(claims).Claims(userClaims).CompactSerialize()
309307
if err != nil {
310308
return err
311309
}
312310

313-
// Set cookie.
314-
err = i.setLogonCookie(rw, serialized)
315-
if err != nil {
311+
return i.setLogonCookie(rw, serialized)
312+
}
313+
314+
// SetUserToLogonCookie serializes the provided user into an encrypted string,
315+
// sets it as cookie on the provided http.ResponseWriter, and fires the
316+
// onSetLogon callbacks.
317+
func (i *Identifier) SetUserToLogonCookie(ctx context.Context, rw http.ResponseWriter, user *IdentifiedUser) error {
318+
if err := i.WriteLogonCookie(rw, user); err != nil {
316319
return err
317320
}
318321
// Trigger callbacks.
319322
for _, f := range i.onSetLogonCallbacks {
320-
err = f(ctx, rw, user)
321-
if err != nil {
323+
if err := f(ctx, rw, user); err != nil {
322324
return err
323325
}
324326
}
325-
326327
return nil
327328
}
328329

identifier/user.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,12 @@ func (u *IdentifiedUser) LoggedOn() (bool, time.Time) {
146146
return !u.logonAt.IsZero(), u.logonAt
147147
}
148148

149+
// SetLogonAt sets the logon timestamp, marking the user as logged on. Used by
150+
// flows that authenticate without a password challenge (e.g. signed login).
151+
func (u *IdentifiedUser) SetLogonAt(t time.Time) {
152+
u.logonAt = t
153+
}
154+
149155
// SessionRef returns the accociated users underlaying session reference.
150156
func (u *IdentifiedUser) SessionRef() *string {
151157
return u.sessionRef

identity/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,8 @@ type Config struct {
3030

3131
ScopesSupported []string
3232

33+
// AllowSignedLogin enables the signed JWT auto sign-in flow.
34+
AllowSignedLogin bool
35+
3336
Logger logrus.FieldLogger
3437
}

0 commit comments

Comments
 (0)