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 &mdash; 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 &mdash; 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 &mdash; 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 &mdash; 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 &mdash;
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 &mdash; 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 &mdash; distinct meanings.</b> Return {@link Optional#empty()}
154    * <b>only</b> for absent / already-consumed / (optionally) store-expired states &mdash; 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 &mdash; 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 &mdash; 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}