oidc: one verifier per issuer, because IdPs mint one per application
Authentik derives the issuer from the application slug, so two applications mean two issuers - and a token's `iss` must match whoever signed it. A single pinned issuer could therefore only ever serve one of the two clients. So there is a verifier per issuer, and each accepts only the client belonging to it. That is tighter than the previous arrangement as well as more general: a token minted for the phone cannot be replayed at the admin login, and vice versa, because they arrive at different verifiers with different audiences. ECHOLOT_OIDC_APP_ISSUER is optional - empty means both clients share ECHOLOT_OIDC_ISSUER, which is what IdPs with one global issuer do. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
5d7f59a66a
commit
c7750fbf0b
@@ -184,22 +184,33 @@ func serve(cfg *config.Config) error {
|
||||
// Identity is optional. Without an issuer the server simply has no sign-in, and
|
||||
// uploads=account can never be satisfied — which is the honest outcome, not a silent
|
||||
// downgrade to anonymous.
|
||||
var idp *oidc.Verifier
|
||||
if cfg.OIDCIssuer != "" && (cfg.OIDCClientID != "" || cfg.OIDCAppClientID != "") {
|
||||
// One verifier per issuer. An IdP may mint a distinct issuer per application — Authentik
|
||||
// derives it from the application slug — and a token's `iss` must match whoever signed it.
|
||||
// Each verifier accepts only the client belonging to its own issuer, so a token minted for
|
||||
// the phone cannot be replayed at the admin login and vice versa.
|
||||
var idp, adminIdP *oidc.Verifier
|
||||
appIssuer := cfg.OIDCAppIssuer
|
||||
if appIssuer == "" {
|
||||
appIssuer = cfg.OIDCIssuer // IdPs with one global issuer
|
||||
}
|
||||
if appIssuer != "" && cfg.OIDCAppClientID != "" {
|
||||
idp = oidc.New(oidc.Config{
|
||||
Issuer: cfg.OIDCIssuer,
|
||||
ClientID: cfg.OIDCClientID,
|
||||
AppClientID: cfg.OIDCAppClientID,
|
||||
AdminGroup: cfg.OIDCAdminGroup,
|
||||
Issuer: appIssuer, AppClientID: cfg.OIDCAppClientID, AdminGroup: cfg.OIDCAdminGroup,
|
||||
}, nil)
|
||||
slog.Info("identity provider configured", "issuer", cfg.OIDCIssuer,
|
||||
"admin_client_id", cfg.OIDCClientID, "app_client_id", cfg.OIDCAppClientID,
|
||||
"admin_group", cfg.OIDCAdminGroup)
|
||||
slog.Info("identity: app client", "issuer", appIssuer, "client_id", cfg.OIDCAppClientID)
|
||||
}
|
||||
if cfg.OIDCIssuer != "" && cfg.OIDCClientID != "" {
|
||||
adminIdP = oidc.New(oidc.Config{
|
||||
Issuer: cfg.OIDCIssuer, ClientID: cfg.OIDCClientID, AdminGroup: cfg.OIDCAdminGroup,
|
||||
}, nil)
|
||||
slog.Info("identity: admin client", "issuer", cfg.OIDCIssuer,
|
||||
"client_id", cfg.OIDCClientID, "admin_group", cfg.OIDCAdminGroup)
|
||||
if cfg.OIDCAdminGroup == "" {
|
||||
slog.Warn("no admin group set: nobody will be an admin via OIDC " +
|
||||
"(set ECHOLOT_OIDC_ADMIN_GROUP)")
|
||||
}
|
||||
} else if cfg.UploadsMode == string(runs.ModeAccount) {
|
||||
}
|
||||
if idp == nil && adminIdP == nil && cfg.UploadsMode == string(runs.ModeAccount) {
|
||||
slog.Warn("uploads=account but no identity provider is configured — " +
|
||||
"every upload will be refused")
|
||||
}
|
||||
@@ -216,6 +227,7 @@ func serve(cfg *config.Config) error {
|
||||
AppRange: appRange,
|
||||
PublicControlURL: publicControlURL(cfg),
|
||||
OIDC: idp,
|
||||
AdminOIDC: adminIdP,
|
||||
}
|
||||
// Left nil when there is no raw socket, so the handler answers "not implemented" with a
|
||||
// reason rather than failing somewhere deeper.
|
||||
|
||||
@@ -74,6 +74,12 @@ type Config struct {
|
||||
OIDCIssuer string // ECHOLOT_OIDC_ISSUER / --oidc-issuer
|
||||
OIDCClientID string // ECHOLOT_OIDC_CLIENT_ID / --oidc-client-id (confidential, admin UI)
|
||||
OIDCAppClientID string // ECHOLOT_OIDC_APP_CLIENT_ID / --oidc-app-client-id (public, the phone app)
|
||||
// Issuer for the app's client, when the IdP gives each application its own.
|
||||
//
|
||||
// Authentik derives the issuer from the application slug, so two applications mean two
|
||||
// issuers — and a token's `iss` must match the one that minted it. Empty means both clients
|
||||
// share ECHOLOT_OIDC_ISSUER, which is what IdPs with a single global issuer do.
|
||||
OIDCAppIssuer string // ECHOLOT_OIDC_APP_ISSUER / --oidc-app-issuer
|
||||
OIDCAdminGroup string // ECHOLOT_OIDC_ADMIN_GROUP / --oidc-admin-group
|
||||
|
||||
// Break-glass admin username; the password lives hashed in the state store.
|
||||
@@ -167,6 +173,7 @@ func Load(args []string) (*Config, *Actions, error) {
|
||||
fs.StringVar(&c.OIDCIssuer, "oidc-issuer", envOr("OIDC_ISSUER", ""), "OpenID Connect issuer URL; empty disables sign-in")
|
||||
fs.StringVar(&c.OIDCClientID, "oidc-client-id", envOr("OIDC_CLIENT_ID", ""), "confidential OIDC client id for the admin UI")
|
||||
fs.StringVar(&c.OIDCAppClientID, "oidc-app-client-id", envOr("OIDC_APP_CLIENT_ID", ""), "public OIDC client id used by the Android app (PKCE)")
|
||||
fs.StringVar(&c.OIDCAppIssuer, "oidc-app-issuer", envOr("OIDC_APP_ISSUER", ""), "issuer for the app client when the IdP uses per-application issuers; empty = same as --oidc-issuer")
|
||||
fs.StringVar(&c.OIDCAdminGroup, "oidc-admin-group", envOr("OIDC_ADMIN_GROUP", ""), "group claim required for admin access; empty means nobody is an admin via OIDC")
|
||||
fs.StringVar(&c.OIDCClientSecret, "oidc-client-secret", secretOr("OIDC_CLIENT_SECRET", ""), "secret for the confidential admin client; prefer ECHOLOT_OIDC_CLIENT_SECRET_FILE")
|
||||
fs.StringVar(&c.AdminBaseURL, "admin-base-url", envOr("ADMIN_BASE_URL", ""), "public URL of the admin UI, for the OIDC redirect (e.g. https://admin.example.net)")
|
||||
|
||||
@@ -59,8 +59,12 @@ type Server struct {
|
||||
// Granted server->client sends (spec §5). Both consume an asymmetric grant.
|
||||
DownTrain func(sess *session.Session, g *session.Grant, count, sizeBytes, intervalUs int) (int, error)
|
||||
BigSend func(sess *session.Session, g *session.Grant, sizes []int, df bool) ([]dataplane.BigSendResult, error)
|
||||
// OIDC verifies ID tokens when the operator has configured an issuer (may be nil).
|
||||
// OIDC verifies ID tokens presented by the *app* (may be nil).
|
||||
OIDC *oidc.Verifier
|
||||
// AdminOIDC verifies tokens from the admin UI's own client. Separate because an IdP may
|
||||
// give each application its own issuer — Authentik derives it from the application slug —
|
||||
// and a verifier pins exactly one issuer and the clients belonging to it.
|
||||
AdminOIDC *oidc.Verifier
|
||||
// Runs stores uploaded measurement documents (may be nil: uploads unsupported).
|
||||
Runs *runs.Store
|
||||
// FragSend emits one datagram as hand-built IP fragments in a chosen order (may be nil:
|
||||
|
||||
Reference in New Issue
Block a user