001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.rest.server.auth.oidc.rp; 018 019import java.time.*; 020import java.util.*; 021 022/** 023 * SPI for the single-use, TTL-bounded {@code state} → ({@code nonce}, PKCE {@code code_verifier}, 024 * redirect target) association created during {@link OidcRelyingParty#startLogin} and consumed during 025 * {@link OidcRelyingParty#completeLogin}. 026 * 027 * <p> 028 * The shipped default is {@link InMemoryLoginStateStore} (per-JVM, bounded, TTL-swept). Supply a shared 029 * (Redis / JDBC) implementation via {@link OidcRelyingParty.Builder#loginStateStore(LoginStateStore)} to 030 * support a clustered deployment where {@code /login} and {@code /callback} may land on different nodes 031 * without sticky sessions — the same motivation as a distributed {@link SessionStore}. When the 032 * hook is unused, behavior is identical to the single-node default. 033 * 034 * <h5 class='section'>Security contract (read before implementing)</h5> 035 * 036 * <p> 037 * <b>Secret-bearing.</b> A stored entry carries the PKCE {@code code_verifier} (and {@code nonce}). With a 038 * stolen authorization {@code code} (Referer, proxy log, browser history) the {@code code_verifier} IS the 039 * token-exchange credential. The in-memory default keeps it on the JVM heap; a shared (Redis / JDBC) 040 * implementation writes it to an external backend. Implementations therefore MUST protect confidentiality 041 * and integrity: TLS in transit and a locked-down ACL are the floor; application-level encryption-at-rest 042 * is strongly recommended. Implementations and callers MUST NOT log a {@link PendingLogin} (its 043 * {@code toString()} is redacted for exactly this reason), and MUST NOT put {@link PendingLogin#codeVerifier()} 044 * or {@link PendingLogin#nonce()} into MDC, metric tags, or field-wise serializers — the accessors 045 * exist only for the token exchange and the ID-token {@code nonce} check. 046 * 047 * <p> 048 * <b>The framework does not trust the store as an oracle.</b> A writable shared store (mis-ACL, 049 * SSRF-to-Redis, replica, backup restore, compromised sibling service) can mint or alter records. The 050 * relying party therefore re-validates on consume: it re-sanitizes {@code redirectTarget} to a safe 051 * relative path before redirecting (a tampered absolute / external URL is discarded in favor of the 052 * configured post-login default), and it re-checks {@code expiresAt} against its own {@link java.time.Clock}. 053 * The store MAY evict early; it is NOT the authoritative expiry decision-maker. Note this defeats an 054 * <i>honest</i> store that merely forgot to expire a record (past {@code expiresAt}); a malicious rewrite 055 * of {@code expiresAt} to a far-future instant is the same trust boundary as writing a fresh forged blob 056 * (an integrity concern an HMAC would address), which the framework does not attempt to defeat here. 057 * 058 * <p> 059 * <b>Thread-safe.</b> Implementations MUST be safe for concurrent {@code store} / {@code consume} from 060 * multiple request threads (as {@link SessionStore} requires). 061 * 062 * <p> 063 * <b>Per-process by default.</b> The shipped default enforces single-use only within one JVM. Cross-node 064 * single-use requires a shared backend with an atomic consume primitive (see {@link #consume(String)}). 065 * 066 * <h5 class='section'>See Also:</h5><ul> 067 * <li class='jc'>{@link InMemoryLoginStateStore} 068 * <li class='jc'>{@link OidcRelyingParty.Builder#loginStateStore(LoginStateStore)} 069 * <li class='jc'>{@link SessionStore} 070 * </ul> 071 * 072 * @since 10.0.0 073 */ 074public interface LoginStateStore { 075 076 /** Hard cap on the framework-enforced login-state TTL — the single public home for the ceiling. */ 077 Duration MAX_TTL = Duration.ofMinutes(30); 078 079 /** 080 * A pending-login association awaiting the IdP callback. 081 * 082 * <p> 083 * <b>Secret-bearing record.</b> {@code codeVerifier} and {@code nonce} are credentials (see the SPI 084 * security contract). {@code toString()} redacts both, matching {@code OAuthToken.toString()} and the 085 * MCP {@code PendingAuthorization} precedent, so a stray log line, exception message, or debugger dump 086 * does not disclose them. 087 * 088 * <p> 089 * <b>Framework-minted timestamps.</b> The relying party stamps {@code createdAt} (from its configured 090 * {@link java.time.Clock}) and {@code expiresAt} ({@code createdAt} plus the RP's capped 091 * {@code stateNonceTtl}). A store MUST persist these as opaque values and MUST NOT recompute them at 092 * consume time — the RP re-checks {@code expiresAt} independently, so an honest store that forgot 093 * to expire a record cannot extend usability. Either timestamp may be {@code null} only for a foreign / 094 * deserialized blob; the RP treats a {@code null} {@code createdAt} / {@code expiresAt} on consume as 095 * expired (fail-closed), so no compact-constructor {@code requireNonNull} is imposed here. 096 * 097 * @param nonce The OIDC {@code nonce}. Redacted in {@code toString()}. 098 * @param codeVerifier The PKCE {@code code_verifier}. Redacted in {@code toString()}. 099 * @param redirectTarget The post-login redirect target. May be {@code null}. Re-validated by the RP. 100 * @param createdAt Framework-minted store instant. 101 * @param expiresAt Framework-minted expiry ceiling ({@code createdAt + stateNonceTtl}). 102 */ 103 record PendingLogin(String nonce, String codeVerifier, String redirectTarget, Instant createdAt, Instant expiresAt) { 104 @Override 105 public String toString() { 106 return "PendingLogin(nonce=<redacted>,codeVerifier=<redacted>,redirectTarget=" + redirectTarget 107 + ",createdAt=" + createdAt + ",expiresAt=" + expiresAt + ")"; 108 } 109 } 110 111 /** 112 * Stores a framework-built pending-login association keyed by {@code state}. 113 * 114 * <p> 115 * Implementations MUST persist the association durably enough that a subsequent {@link #consume(String)} 116 * on any node handling the callback can retrieve it. On an <b>infrastructure failure</b> (backend 117 * unreachable, write rejected) implementations MUST throw rather than silently drop the record: 118 * {@code store} is invoked on the {@code startLogin} leg before any authorization URL is issued, so a 119 * thrown exception fails the flow closed with no half-initiated login. Implementations MUST NOT log the 120 * {@link PendingLogin} payload; wrap / sanitize backend exceptions so the {@code codeVerifier} / 121 * {@code state} do not leak into a 500 body or log. 122 * 123 * <p> 124 * Last-writer-wins: a repeated {@code store} for the same {@code state} overwrites (a Nimbus 125 * {@code State} collision is astronomical). A backend MAY use {@code SET NX} but is not required to. A 126 * backend MAY use the remaining time to {@code expiresAt} as a GC / {@code EXPIRE} hint, but GC failure 127 * MUST NOT extend usability past the framework's re-checked {@code expiresAt}. 128 * 129 * @param state The opaque {@code state} value. Must not be {@code null} or blank. 130 * @param pending The framework-built association (already stamped with {@code createdAt} / {@code expiresAt}). 131 * Must not be {@code null}. 132 */ 133 void store(String state, PendingLogin pending); 134 135 /** 136 * Atomically removes and returns the pending-login association for {@code state}, if present. 137 * 138 * <p> 139 * <b>Single-use / atomicity is mandatory (TOCTOU).</b> The removal and the read MUST be a single atomic 140 * operation so a given {@code state} is consumed at most once. Of any two concurrent calls with the same 141 * {@code state}, <b>at most one</b> returns a present {@link Optional}; the other MUST miss — 142 * including across nodes sharing the store. Use an atomic primitive against the primary (Redis 143 * {@code GETDEL} or a Lua {@code GET}+{@code DEL}), <b>not</b> a read-then-delete and <b>not</b> a 144 * possibly-stale replica read. A consume that removes the entry but fails to return it (lost response) 145 * MUST be treated as a miss on retry — never a best-effort put-back (put-back reopens replay); the 146 * user simply restarts login. 147 * 148 * <p> 149 * <b>Bounded I/O.</b> Implementations MUST apply a bounded deadline to the backend round-trip so a hung 150 * backend cannot stall {@code completeLogin} on a request thread indefinitely. 151 * 152 * <p> 153 * <b>{@code empty()} vs. throw — distinct meanings.</b> Return {@link Optional#empty()} 154 * <b>only</b> for absent / already-consumed / (optionally) store-expired states — the caller 155 * treats {@code empty()} as a CSRF / replay / expiry rejection. On an <b>infrastructure failure</b> 156 * (backend unreachable) the implementation MUST throw (not return {@code empty()}) so the caller can 157 * distinguish an attack from an outage; the RP catches it, logs without the payload, and rejects with an 158 * "unavailable" authentication error. Either way the login fails closed; implementations MUST NEVER 159 * fabricate a {@link PendingLogin}. 160 * 161 * <p> 162 * <b>Expiry is not the store's sole responsibility.</b> An implementation MAY treat past-{@code expiresAt} 163 * entries as a miss (early eviction), but the RP re-checks {@code expiresAt} on the returned record 164 * regardless, so it is not a security defect if a store returns an over-age record — the framework 165 * rejects it. 166 * 167 * @param state The {@code state} value from the callback. May be {@code null} (returns {@code empty()}, 168 * matching the defensive callback path — do not throw). 169 * @return The association, or {@link Optional#empty()} if absent, expired, or already consumed. 170 */ 171 Optional<PendingLogin> consume(String state); 172}