oauth-as 0.9.3

An embeddable OAuth 2.1 Authorization Server library: spec-mirroring types (RFC 6749, RFC 8628, RFC 7636), a full device-authorization-grant state machine, and a storage trait the host implements. Deliberately host-agnostic with a tiny dependency set; nothing is allocated until the host constructs an AuthorizationServer, so an embedding host pays zero memory until its config enables the feature.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
// SPDX-License-Identifier: MIT OR Apache-2.0
// Copyright (C) 2026 Matthew Jackson

//! The authorization endpoint (RFC 6749 section 4.1) under the OAuth 2.1 constraints: `code` is
//! the only response type (the implicit grant is removed), PKCE is REQUIRED, only `S256` is
//! offered, and registered redirect URIs match exactly.
//!
//! # Why the request type is lenient
//!
//! [`AuthorizationRequest`] holds raw, optional text rather than parsed enums. A server cannot
//! reject what it cannot represent, and rejecting correctly is most of what this endpoint does:
//! a request with no `code_challenge`, or with `response_type=token`, has to reach the state
//! machine so the machine can answer it the way the RFC prescribes. Parsing into
//! [`ValidatedAuthorizationRequest`] is what validation MEANS here, and only the validated form
//! can mint a code. [`ValidatedAuthorizationRequest`] carries a private witness field so that,
//! within this crate, `ValidatedAuthorizationRequest::new` (private) is the only way to produce one; a
//! host consuming this crate cannot construct one for itself. See that type's doc comment for
//! exactly what is and is not proven by this.
//!
//! # Why the two error shapes are different
//!
//! RFC 6749 section 4.1.2.1 splits authorization errors in two, and the split is a security
//! boundary rather than a stylistic one. If the client or the redirect URI cannot be validated,
//! the AS MUST NOT redirect: the redirect target is precisely what an attacker would be trying to
//! choose, so reporting the error to it would hand over the thing being protected. Every other
//! error goes back to the (already validated) redirect URI as query parameters.
//!
//! # Allocation
//!
//! Request fields are [`Cow`], so a host parsing a query string that needs no percent-decoding
//! borrows it and allocates nothing. Only the validated form, which outlives the request while a
//! consent screen is shown, owns its data.

use std::borrow::Cow;
use std::fmt;
use std::time::SystemTime;

use serde::{Deserialize, Serialize};

use crate::client::ClientId;
use crate::error::{ErrorCode, ErrorResponse};
use crate::scope::ScopeSet;

/// `response_type` values this server will ever accept. OAuth 2.1 removes `token` (implicit).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ResponseType {
    /// The authorization-code response type.
    #[serde(rename = "code")]
    Code,
}

/// PKCE challenge methods this server offers (RFC 7636). OAuth 2.1 requires `S256`; `plain` is
/// deliberately not implemented, and is not advertised, because accepting it is a downgrade.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CodeChallengeMethod {
    /// `code_challenge = BASE64URL(SHA256(ASCII(code_verifier)))`, no padding.
    S256,
}

/// The raw authorization request as it arrives on the wire (RFC 6749 section 4.1.1 plus the RFC
/// 7636 PKCE parameters). The host parses its query string into this; every member is optional
/// because every member can be absent in a real (invalid) request.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
/// `#[non_exhaustive]`: `rar` adds `authorization_details` and `consent` adds two more, and the
/// doc above explains why they live here rather than being read off the query separately, which
/// means this type is where every future authorization parameter lands as well.
///
/// [`AuthorizationRequest::from_pairs`] is the path a host actually wants: it takes the decoded
/// query pairs and applies the section 3.1 rules about unknown and repeated parameters, which a
/// struct literal assembled by hand does not. For a request built in code rather than parsed, start
/// from `Default::default()` and assign; every field is public and every field is legitimately
/// absent, so there is nothing a literal could express that this cannot.
#[non_exhaustive]
pub struct AuthorizationRequest<'a> {
    /// Must be `code`.
    pub response_type: Option<Cow<'a, str>>,
    /// The requesting client.
    pub client_id: Option<Cow<'a, str>>,
    /// Requested redirect target; must exact-match a registered URI when present.
    pub redirect_uri: Option<Cow<'a, str>>,
    /// Requested scope, space delimited; absent means the client's registered default.
    pub scope: Option<Cow<'a, str>>,
    /// Opaque client state, echoed back verbatim.
    pub state: Option<Cow<'a, str>>,
    /// The PKCE challenge (REQUIRED in OAuth 2.1 for the authorization code grant).
    pub code_challenge: Option<Cow<'a, str>>,
    /// The PKCE method; only `S256`.
    pub code_challenge_method: Option<Cow<'a, str>>,
    /// RFC 8707 resource indicators: the resource server(s) the client intends the issued token to
    /// be used at.
    ///
    /// A `Vec` rather than an `Option<Cow>` because RFC 8707 section 2 says the parameter MAY be
    /// repeated, and a client naming two resource servers means both, not the last one. An empty
    /// `Vec` allocates nothing, so a request that carries no `resource` still costs what it did
    /// before this parameter existed.
    pub resource: Vec<Cow<'a, str>>,
    /// RFC 9396 section 2 `authorization_details`: a JSON array of objects, each naming a
    /// `type` that defines the rest of it.
    ///
    /// RAW TEXT, not a parsed structure, for the reason the rest of this type is raw text:
    /// a server cannot reject what it cannot represent, and this parameter's failure modes
    /// (unparseable, oversized, an unknown type) all have to reach the state machine so it
    /// can answer them the way RFC 9396 section 5 prescribes. Parsing happens in
    /// [`crate::rar::AuthorizationDetails::parse`], under this crate's bounds, and only the
    /// validated form carries the result.
    ///
    /// NOT FEATURE GATED, which is the same decision [`crate::ErrorCode::InvalidAuthorizationDetails`]
    /// records for the error code and taken for the same reason. Without `rar` this crate supports
    /// no authorization detail type at all, so RFC 9396 section 5's condition is met by EVERY
    /// request that carries the parameter and every one of them has to be refused. A field that
    /// disappeared with the feature left the parameter nowhere to land, and a parameter that lands
    /// nowhere is a parameter accepted and ignored, which is the one outcome section 5 forbids.
    /// So the field exists in every build; what changes with the feature is whether the value is
    /// honoured or refused, and that is decided during validation, not during parsing.
    pub authorization_details: Option<Cow<'a, str>>,
    /// RFC 9470 section 4 / OpenID Connect Core section 3.1.2.1 `acr_values`: the authentication
    /// context classes the client will accept, space delimited, in order of preference.
    ///
    /// ON THIS TYPE rather than parsed straight off the query, and that is the whole of the fix
    /// for a real gap. Every way into the authorization endpoint (a plain query, an RFC 9126
    /// pushed request, an RFC 9101 signed request object) funnels through this struct, so a
    /// parameter that lives here is a parameter every path must carry; a parameter parsed
    /// separately from the query is a parameter the other two paths silently drop. That is exactly
    /// what happened to these two: PAR and JAR requests disabled step-up entirely, and for JAR the
    /// server was reading intermediary-rewritable query text on a request whose only purpose is
    /// that it cannot be rewritten (RFC 9101 section 6.3).
    ///
    /// RAW TEXT for the same reason as the rest of this type, and parsed into
    /// [`crate::consent::AuthenticationRequirement`] by validation.
    #[cfg(feature = "consent")]
    pub acr_values: Option<Cow<'a, str>>,
    /// RFC 9470 section 4 / OpenID Connect Core section 3.1.2.1 `max_age`: how old the user's
    /// authentication may be, in seconds. See [`AuthorizationRequest::acr_values`] for why it is
    /// a field here rather than something read off the query.
    #[cfg(feature = "consent")]
    pub max_age: Option<Cow<'a, str>>,
}

impl<'a> AuthorizationRequest<'a> {
    /// Collect a request from already-decoded `(name, value)` query pairs.
    ///
    /// Unknown parameters are ignored, which RFC 6749 section 3.1 requires. A repeated parameter
    /// keeps the FIRST occurrence: section 3.1 says a parameter MUST NOT appear more than once,
    /// and last-wins is the smuggling-friendly choice when two intermediaries disagree about
    /// which copy counts.
    pub fn from_pairs<I, K, V>(pairs: I) -> Self
    where
        I: IntoIterator<Item = (K, V)>,
        K: AsRef<str>,
        V: Into<Cow<'a, str>>,
    {
        let mut req = AuthorizationRequest::default();
        for (k, v) in pairs {
            // RFC 8707 section 2 is the one exception to the first-wins rule below: `resource` MAY
            // legitimately appear more than once, and every occurrence is part of the request.
            if k.as_ref() == "resource" {
                req.resource.push(v.into());
                continue;
            }
            let slot = match k.as_ref() {
                "response_type" => &mut req.response_type,
                "client_id" => &mut req.client_id,
                "redirect_uri" => &mut req.redirect_uri,
                "scope" => &mut req.scope,
                "state" => &mut req.state,
                "code_challenge" => &mut req.code_challenge,
                "code_challenge_method" => &mut req.code_challenge_method,
                // Ungated, with the field: a build without `rar` has to KNOW the parameter was
                // sent in order to refuse it (RFC 9396 s5). Falling through to `_ => continue`
                // here is what made this the accept-and-ignore path for `GET /authorize` and, via
                // `crate::par`, for the RFC 9126 push as well.
                "authorization_details" => &mut req.authorization_details,
                #[cfg(feature = "consent")]
                "acr_values" => &mut req.acr_values,
                #[cfg(feature = "consent")]
                "max_age" => &mut req.max_age,
                _ => continue,
            };
            if slot.is_none() {
                *slot = Some(v.into());
            }
        }
        req
    }
}

/// Whether one RFC 8707 `resource` value is a resource indicator this server will accept.
///
/// RFC 8707 section 2 states the rule in three parts, and all three are checked here:
///
/// 1. the value MUST be an absolute URI as defined by RFC 3986 section 4.3, so it carries a scheme
///    (`scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )`) followed by `:`. A relative reference
///    names nothing an audience restriction could be built from, since what it resolves against is
///    the client's business and not the server's;
/// 2. it MAY include a query component, so `?` is explicitly NOT a reason to refuse. That is worth
///    stating because the obvious "strip everything after the first delimiter" implementation gets
///    it wrong;
/// 3. it MUST NOT include a fragment. A fragment is never transmitted to a server (RFC 3986
///    section 3.5), so two indicators differing only in their fragment name the SAME resource while
///    comparing unequal, which turns any later audience check into a string game.
///
/// Anything outside printable ASCII is refused as well: RFC 3986 section 2 requires characters
/// outside its own grammar to be percent-encoded, so a raw space or control byte here is not a URI
/// at all, and accepting one would let a value that cannot round-trip through a query string reach
/// the token's audience.
pub(crate) fn is_valid_resource_indicator(value: &str) -> bool {
    let bytes = value.as_bytes();
    // Scheme: at least one character, the first alphabetic, terminated by the first `:`.
    let colon = match value.find(':') {
        Some(0) | None => return false,
        Some(i) => i,
    };
    if !bytes[0].is_ascii_alphabetic() {
        return false;
    }
    if !bytes[1..colon]
        .iter()
        .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'+' | b'-' | b'.'))
    {
        return false;
    }
    // No fragment, and nothing that is not a legal URI character in the first place.
    bytes
        .iter()
        .all(|&b| b != b'#' && (0x21..=0x7e).contains(&b))
}

/// A request that has passed validation: the client exists, is allowed this grant, the redirect
/// URI is one of its registrations, and the PKCE parameters are well formed.
///
/// The data fields stay `pub` for read access (state, ergonomics, and the smallest diff over the
/// existing call sites in `tests/authorization_code.rs`, none of which construct this type by
/// hand). What actually enforces "only a validated request can mint a code" is the private
/// `_sealed` field below: because it is not `pub`, no struct-literal expression written outside
/// this module (that includes every downstream host, since this module is the only one with
/// access to `Sealed`) can name every field of this struct, so a `ValidatedAuthorizationRequest`
/// can only come from the crate-private `ValidatedAuthorizationRequest::new`, which only
/// `AuthorizationServer::validate_authorization_request` (in `server.rs`, within this crate)
/// calls. That is what "cannot be spelled" now actually means: not "the fields are private" (they
/// are not), but "the value cannot be produced without going through validation first."
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValidatedAuthorizationRequest {
    /// The validated client.
    pub client_id: ClientId,
    /// The exact registered redirect URI this request resolved to.
    pub redirect_uri: String,
    /// Whether the authorization REQUEST named the redirect URI itself, as opposed to omitting it
    /// and being filled in from the client's single registration (RFC 6749 section 3.1.2.3).
    ///
    /// Carried onto [`AuthorizationCodeRecord::redirect_uri_was_explicit`], because RFC 6749
    /// section 4.1.3 makes the token endpoint's `redirect_uri` parameter REQUIRED "if the
    /// `redirect_uri` parameter was included in the authorization request" and not otherwise. The
    /// resolved URI above cannot answer that: it is filled in either way, so by the time the token
    /// endpoint sees the record the two cases are indistinguishable without this.
    pub redirect_uri_was_explicit: bool,
    /// The scope that will be granted on approval.
    pub scope: ScopeSet,
    /// The request's `state`, to be echoed on either outcome.
    pub state: Option<String>,
    /// The PKCE challenge to record against the issued code.
    pub code_challenge: String,
    /// The PKCE method (only `S256`).
    pub code_challenge_method: CodeChallengeMethod,
    /// This server's issuer identifier, carried so that the authorization response can name its
    /// author (RFC 9207 section 2).
    ///
    /// It is on the VALIDATED request rather than looked up at response time because
    /// [`ValidatedAuthorizationRequest::denied`] is also an authorization response and has no
    /// access to the server's configuration: a refusal that could not say who refused would be
    /// exactly the gap RFC 9207 section 2.2 closes.
    pub issuer: String,
    /// The RFC 8707 resource indicators this request asked for, already validated. Empty when the
    /// client named none, which means the issued token carries no audience restriction from this
    /// mechanism.
    pub resource: Vec<String>,
    /// The RFC 9396 authorization details this request asked for, already parsed and
    /// already checked against the server's supported types (section 5). Empty when the
    /// client named none.
    ///
    /// A host's consent screen MAY replace this before the code is issued: RFC 9396 section
    /// 7.1 is explicit that the details attached to the token may differ from the request,
    /// which is how an AS records the account the user actually picked.
    #[cfg(feature = "rar")]
    pub authorization_details: crate::rar::AuthorizationDetails,
    /// The RFC 9470 step-up requirement this request carried, already parsed.
    ///
    /// It rides on the VALIDATED request so that there is exactly ONE source of it per path, and
    /// that source is the request that was actually resolved: the pushed record for a PAR request,
    /// the signed claim set for a JAR request, the query for a plain one. Anything that reads the
    /// query separately is reading the wrong thing for two of the three (RFC 9126 section 4, RFC
    /// 9101 section 6.3).
    ///
    /// Empty means the client asked for no step-up, which is what an ordinary request carries.
    #[cfg(feature = "consent")]
    pub authentication_requirement: crate::consent::AuthenticationRequirement,
    /// Zero-sized witness, private to this module. Its only purpose is that it cannot be named
    /// (let alone constructed) outside `authorization.rs`, so a struct-literal expression cannot
    /// build a whole `ValidatedAuthorizationRequest` from anywhere else, in this crate or out of
    /// it. See the struct doc comment.
    _sealed: Sealed,
}

/// The witness type behind [`ValidatedAuthorizationRequest`]'s sealed field. Deliberately
/// private and zero-sized: it carries no data and exists only to make the containing struct
/// unconstructible by struct-literal syntax from outside this module.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Sealed;

impl ValidatedAuthorizationRequest {
    /// The only constructor. `pub(crate)` rather than private: `AuthorizationServer::
    /// validate_authorization_request` lives in `server.rs`, a sibling module, and is the sole
    /// intended caller. `pub(crate)` is strictly narrower than the old fully-`pub` struct
    /// literal: no code outside this crate can reach this function, so no host can hand
    /// [`crate::server::AuthorizationServer::issue_authorization_code`] a request it invented
    /// itself. (Within the crate, `pub(crate)` cannot stop a different in-crate module from also
    /// calling this constructor honestly; the guarantee this seals is against a host of the
    /// library, not against other code inside it.)
    #[allow(clippy::too_many_arguments)]
    pub(crate) fn new(
        client_id: ClientId,
        redirect_uri: String,
        // An ARGUMENT rather than a setter with a default, unlike the two `set_*` methods below.
        // Those record something the request may simply not have carried; this one is a fact about
        // every authorization request there is, and the token endpoint's refusal turns on it. A
        // default would be a value the caller could forget to correct, in the one place where
        // getting it wrong either refuses a conforming client or waives a check RFC 6749 section
        // 4.1.3 requires.
        redirect_uri_was_explicit: bool,
        scope: ScopeSet,
        state: Option<String>,
        code_challenge: String,
        code_challenge_method: CodeChallengeMethod,
        issuer: String,
        resource: Vec<String>,
    ) -> Self {
        ValidatedAuthorizationRequest {
            client_id,
            redirect_uri,
            redirect_uri_was_explicit,
            scope,
            state,
            code_challenge,
            code_challenge_method,
            issuer,
            resource,
            #[cfg(feature = "rar")]
            authorization_details: crate::rar::AuthorizationDetails::none(),
            #[cfg(feature = "consent")]
            authentication_requirement: crate::consent::AuthenticationRequirement::none(),
            _sealed: Sealed,
        }
    }

    /// Record the RFC 9470 step-up requirement this request was validated as carrying.
    ///
    /// `pub(crate)` for the reason [`ValidatedAuthorizationRequest::set_authorization_details`]
    /// is: a public setter for the field that decides whether a code may be minted at all would
    /// hand back exactly what the sealed field exists to keep.
    #[cfg(feature = "consent")]
    pub(crate) fn set_authentication_requirement(
        &mut self,
        requirement: crate::consent::AuthenticationRequirement,
    ) {
        self.authentication_requirement = requirement;
    }

    /// Record the RFC 9396 authorization details this request was validated as carrying.
    ///
    /// `pub(crate)`, like [`ValidatedAuthorizationRequest::new`] itself: the sealed field on
    /// this type exists so that only validation can produce one, and a public setter for a
    /// field that decides what a token authorizes would hand that back.
    #[cfg(feature = "rar")]
    pub(crate) fn set_authorization_details(&mut self, details: crate::rar::AuthorizationDetails) {
        self.authorization_details = details;
    }

    /// The redirect describing the user refusing consent (RFC 6749 section 4.1.2.1
    /// `access_denied`). A refusal is an answer the client is entitled to receive, not an error
    /// page.
    pub fn denied(&self) -> AuthorizationErrorRedirect {
        AuthorizationErrorRedirect {
            redirect_uri: self.redirect_uri.clone(),
            error: ErrorResponse::new(ErrorCode::AccessDenied),
            state: self.state.clone(),
            // RFC 9207 section 2: the parameter belongs on the authorization response whatever the
            // outcome, and a refusal is an outcome the client is entitled to attribute.
            iss: self.issuer.clone(),
        }
    }
}

/// Hand-written so the one-time `code` never prints. `state` and `iss` print in full: `state` is
/// the client's own opaque value echoed back and `iss` is this server's public identifier.
impl fmt::Debug for AuthorizationResponse {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("AuthorizationResponse")
            .field("code", &"[redacted]")
            .field("state", &self.state)
            .field("iss", &self.iss)
            .finish()
    }
}

/// The success redirect parameters (RFC 6749 section 4.1.2).
///
/// `Debug` is HAND-WRITTEN (above) and does not print the `code`. The RECORD form of the same
/// value, [`AuthorizationCodeRecord`], has been hand-redacted since it was written, for the reason
/// stated there -- RFC 6749 section 4.1.2 makes a code a credential in its own right -- and this
/// type, which carries the same string to the client, was left deriving until 0.9.2. A host that
/// logs the response it is about to redirect with would have written a live, unredeemed code into
/// its logs; PKCE binds the code to a DIFFERENT client, not to a reader who has the log and the
/// verifier, and for a confidential client it does not bind at all.
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthorizationResponse {
    /// The single-use authorization code.
    pub code: String,
    /// The request's `state`, echoed verbatim; REQUIRED iff the request carried one.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub state: Option<String>,
    /// RFC 9207 section 2: this server's issuer identifier, so a client talking to several
    /// authorization servers can tell WHICH one answered.
    ///
    /// Not an `Option`: this server always supports RFC 9207 and its metadata says so
    /// (`authorization_response_iss_parameter_supported`), and section 2 makes the parameter
    /// mandatory for a server that does. A response that could omit it would be a response a
    /// client cannot rely on, which defeats the mix-up countermeasure RFC 9700 section 4.4 is
    /// asking for.
    pub iss: String,
}

impl AuthorizationResponse {
    /// The `Location` header value for the success redirect.
    ///
    /// Parameter ORDER is not constrained by any RFC (RFC 6749 section 4.1.2 and RFC 9207
    /// section 2 both describe an unordered set of query parameters), so `iss` is appended last
    /// purely to keep the existing prefix of the URL unchanged.
    pub fn location(&self, redirect_uri: &str) -> String {
        let mut out = String::with_capacity(redirect_uri.len() + self.encoded_len());
        out.push_str(redirect_uri);
        let mut sep = query_separator(redirect_uri);
        append_param(&mut out, &mut sep, "code", &self.code);
        if let Some(state) = &self.state {
            append_param(&mut out, &mut sep, "state", state);
        }
        append_param(&mut out, &mut sep, "iss", &self.iss);
        out
    }

    /// A worst-case size for the appended query, so `location` allocates exactly once.
    fn encoded_len(&self) -> usize {
        6 + self.code.len() * 3
            + self.state.as_ref().map_or(0, |s| 7 + s.len() * 3)
            // "&iss=" is 5 characters, and the issuer is percent-encoded like everything else.
            + 5
            + self.iss.len() * 3
    }
}

/// An authorization error delivered by redirecting the user agent back to the client (RFC 6749
/// section 4.1.2.1).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuthorizationErrorRedirect {
    /// The VALIDATED redirect URI. Never a URI supplied by an unvalidated request.
    pub redirect_uri: String,
    /// The error to report.
    pub error: ErrorResponse,
    /// The request's `state`, echoed so the client can correlate the failure.
    pub state: Option<String>,
    /// RFC 9207 section 2: the issuer identifier, present on error responses exactly as on
    /// successful ones. A client that cannot attribute a failure cannot tell a genuine refusal by
    /// its own AS from one manufactured by an attacker's.
    pub iss: String,
}

impl AuthorizationErrorRedirect {
    /// The `Location` header value for the error redirect.
    pub fn location(&self) -> String {
        // 96 covers the error code, the separators, and a short description; the issuer is sized
        // properly because a host may run under a long issuer identifier and RFC 9207 section 2
        // puts it on every one of these.
        let mut out = String::with_capacity(self.redirect_uri.len() + 96 + self.iss.len() * 3);
        out.push_str(&self.redirect_uri);
        let mut sep = query_separator(&self.redirect_uri);
        append_param(&mut out, &mut sep, "error", self.error.error.as_str());
        if let Some(d) = &self.error.error_description {
            append_param(&mut out, &mut sep, "error_description", d);
        }
        if let Some(u) = &self.error.error_uri {
            append_param(&mut out, &mut sep, "error_uri", u);
        }
        if let Some(state) = &self.state {
            append_param(&mut out, &mut sep, "state", state);
        }
        append_param(&mut out, &mut sep, "iss", &self.iss);
        out
    }
}

/// How an authorization request failed.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthorizationError {
    /// RFC 6749 section 4.1.2.1: the client or the redirect URI could not be validated, so the AS
    /// MUST NOT redirect. The host renders this to the user directly; 400 is the conventional
    /// status.
    Direct(ErrorResponse),
    /// The error is reported to the client by redirecting to its validated redirect URI.
    Redirect(AuthorizationErrorRedirect),
}

impl AuthorizationError {
    /// The HTTP status the host should use: 302 for the redirect form, otherwise a direct status.
    pub fn http_status(&self) -> u16 {
        match self {
            // invalid_client's 401 belongs to the token endpoint's WWW-Authenticate exchange; at
            // the authorization endpoint there is no client authentication to challenge, so a
            // refused request is a plain 400.
            AuthorizationError::Direct(e) if e.error == ErrorCode::ServerError => 500,
            AuthorizationError::Direct(_) => 400,
            AuthorizationError::Redirect(_) => 302,
        }
    }
}

impl std::fmt::Display for AuthorizationError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            AuthorizationError::Direct(e) => write!(f, "{e}"),
            AuthorizationError::Redirect(r) => write!(f, "{}", r.error),
        }
    }
}

impl std::error::Error for AuthorizationError {}

/// What an issued authorization code became, once redeemed.
///
/// Consumed codes are RETAINED until their expiry rather than deleted, because RFC 6749 section
/// 4.1.2 and RFC 9700 section 4.1.1 want a replayed code to revoke the tokens it already minted.
/// Deleting the record on redemption would make a replay indistinguishable from a typo, and the
/// stolen access token would stay live.
/// `Debug` is hand-written (see below), for the same reason as [`crate::client::ClientAuth`]: the
/// `Consumed` variant carries the access and refresh tokens this code minted, and those are bearer
/// credentials that a host's `tracing::debug!(?record)` must not write to a log in plaintext.
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum AuthorizationCodeState {
    /// Issued and not yet redeemed.
    Issued,
    /// Already redeemed, recording what it minted so a replay can revoke it.
    Consumed {
        /// The access token issued, if the issuance got as far as producing one.
        ///
        /// `None` means the code was marked consumed and the issuance that followed did not
        /// complete. That is deliberate and it is not a lost write: the record is written BEFORE
        /// issuance precisely so that a store failure halfway through a redemption cannot take the
        /// replay alarm offline with it (see `AuthorizationServer::authorization_code_token`).
        /// There is genuinely nothing to revoke in that case, because nothing was issued, and a
        /// replay of the code is still recognised as a replay.
        access_token: Option<String>,
        /// The refresh token issued, if any.
        refresh_token: Option<String>,
    },
    /// Consumed, AND presented again afterwards. A detected replay, recorded DURABLY.
    ///
    /// # Why this is a state and not a boolean on the side
    ///
    /// It exists to be read by a redemption that is still running. The interleaving it closes:
    /// redeemer A takes the code, writes `Consumed { access_token: None, .. }` before issuing (so
    /// that a store failure cannot disarm the alarm), and then SUSPENDS on the host's
    /// [`crate::jwt::Es256Signer`], which is a network round trip when that signer fronts a KMS.
    /// Replayer B arrives in that window, finds `Consumed { access_token: None }`, and correctly
    /// concludes there is nothing to revoke, because nothing has been issued YET. B refuses the
    /// replay and puts the record back.
    ///
    /// If what B puts back is `Consumed`, it is byte for byte what A wrote, so when A wakes and
    /// records what it minted, A cannot tell that anything happened. The replay was detected, the
    /// audit event fired, and A's freshly minted access token and refresh chain are live. The
    /// alarm rang and nothing was contained.
    ///
    /// `Replayed` is the trace A can see. A's second write is a compare-and-swap against the
    /// `Consumed` it wrote itself (see [`crate::store::Storage::compare_and_swap_authorization_code`]),
    /// so this state makes it fail, and A undoes its own issuance.
    ///
    /// It carries the same two fields because a THIRD presentation is still a replay and must
    /// still revoke whatever is by then known to have been minted.
    Replayed {
        /// The access token issued, if the redemption that consumed this code got as far as
        /// producing one before the replay was detected.
        access_token: Option<String>,
        /// The refresh token issued, if any.
        refresh_token: Option<String>,
    },
}

impl AuthorizationCodeState {
    /// What this code minted, for the two states that can name it.
    ///
    /// One accessor rather than two matches at each call site: the replay path treats `Consumed`
    /// and `Replayed` identically when deciding what to revoke, and the only difference between
    /// them is which one a concurrent redemption is allowed to overwrite.
    pub fn minted(&self) -> Option<(Option<&str>, Option<&str>)> {
        match self {
            AuthorizationCodeState::Issued => None,
            AuthorizationCodeState::Consumed {
                access_token,
                refresh_token,
            }
            | AuthorizationCodeState::Replayed {
                access_token,
                refresh_token,
            } => Some((access_token.as_deref(), refresh_token.as_deref())),
        }
    }
}

impl fmt::Debug for AuthorizationCodeState {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AuthorizationCodeState::Issued => f.write_str("Issued"),
            // Both carry the same two credentials and redact them the same way. The VARIANT NAME
            // is the part an operator reading a debug dump needs, because it is the difference
            // between a redemption and a detected replay.
            AuthorizationCodeState::Consumed {
                access_token,
                refresh_token,
            }
            | AuthorizationCodeState::Replayed {
                access_token,
                refresh_token,
            } => f
                .debug_struct(match self {
                    AuthorizationCodeState::Replayed { .. } => "Replayed",
                    _ => "Consumed",
                })
                // Presence/absence stays visible for the same reason it does for the refresh token
                // below, and here it carries more: `None` is how a redemption whose issuance failed
                // is told apart from one that completed.
                .field("access_token", &access_token.as_ref().map(|_| "[redacted]"))
                .field(
                    "refresh_token",
                    // Presence/absence is worth keeping visible (it distinguishes an
                    // authorization-code-only grant from one that also minted a refresh token);
                    // the value itself is not.
                    &refresh_token.as_ref().map(|_| "[redacted]"),
                )
                .finish(),
        }
    }
}

/// The serde default for [`AuthorizationCodeRecord::redirect_uri_was_explicit`]: `true`, which is
/// the fail-closed reading of a record written before the field existed. See the field.
fn redirect_uri_was_explicit_default() -> bool {
    true
}

/// The serde default for [`AuthorizationCodeRecord::issued_at`]: the epoch, because it is the
/// fail-closed answer. Every barrier is recorded after it, so a code with no stated decision
/// instant is REFUSED by a standing revocation rather than admitted by one. See the field.
fn grant_instant_default() -> SystemTime {
    SystemTime::UNIX_EPOCH
}

/// A persisted authorization code (RFC 6749 section 4.1.2).
///
/// `Debug` is hand-written (see below): `code` is itself a bearer credential (RFC 6749 section
/// 4.1.2 treats a leaked code as equivalent to a leaked token for as long as it is live, which is
/// why replay revokes what it minted, see [`AuthorizationCodeState`]'s doc comment), so it must
/// not appear in a debug format either.
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
/// `#[non_exhaustive]`: `rar` and `consent` each add a field. Like the two token records this is a
/// value a [`crate::store::Storage`] implementor is HANDED and gives back, through the derived
/// serde impls, which are generated in this crate and keep working from outside it; nothing in
/// `oauth-as-postgres` names a field of it. [`AuthorizationCodeRecord::new`] is for anyone building
/// one directly.
#[non_exhaustive]
pub struct AuthorizationCodeRecord {
    /// The code string (the storage key).
    pub code: String,
    /// The client the code was issued to; presentation by any other client is `invalid_grant`.
    pub client_id: ClientId,
    /// The redirect URI the authorization request used; a token request that presents one must
    /// present this one.
    pub redirect_uri: String,
    /// Whether the authorization request NAMED that URI, rather than omitting it and being filled
    /// in from the client's single registration (RFC 6749 section 3.1.2.3).
    ///
    /// RFC 6749 section 4.1.3 makes the token endpoint's `redirect_uri` parameter REQUIRED "if the
    /// `redirect_uri` parameter was included in the authorization request", and conditional means
    /// conditional in both directions: through 0.9.1 the token endpoint required it always, so a
    /// client entitled by section 3.1.2.3 to omit it at the authorization endpoint — the ordinary
    /// shape for a client with exactly one registered URI — was refused at the token endpoint, and
    /// refused with a message blaming a mismatch that had not happened. `redirect_uri` above cannot
    /// stand in for this, because it is filled in either way.
    ///
    /// `#[serde(default)]` with a `true` default, so a record persisted by 0.9.0 still
    /// deserializes. TRUE is the fail-closed direction: it keeps the check that release performed
    /// (the parameter is required) for records minted before this field existed, rather than
    /// silently waiving section 4.1.3's requirement for every grant that survived the upgrade.
    #[serde(default = "redirect_uri_was_explicit_default")]
    pub redirect_uri_was_explicit: bool,
    /// The scope the user approved.
    pub scope: ScopeSet,
    /// The authenticated resource owner.
    pub subject: String,
    /// The recorded PKCE challenge (RFC 7636 section 4.4).
    pub code_challenge: String,
    /// The recorded PKCE method.
    pub code_challenge_method: CodeChallengeMethod,
    /// The RFC 8707 resource indicators the authorization request named.
    ///
    /// Recorded on the code because the token request that redeems it MAY narrow this set but MUST
    /// NOT widen it (RFC 8707 section 2), and "what was granted" is not knowable at the token
    /// endpoint any other way. Empty means the client asked for no audience restriction.
    pub resource: Vec<String>,
    /// The RFC 9396 authorization details the user approved.
    ///
    /// Recorded on the code for exactly the reason `resource` above is: section 6 lets the
    /// token request that redeems it NARROW this set and never widen it, and "what was
    /// granted" is not knowable at the token endpoint any other way. Empty means the client
    /// asked for no rich authorization detail.
    ///
    /// `#[serde(default)]`, which [`crate::token::IssuedToken::authorization_details`] states in
    /// full: a code written by a build without `rar` carries no such key, this is not an `Option`
    /// so serde supplies no default of its own, and without one every code in flight becomes
    /// unreadable the moment anything in the host's dependency graph turns the feature on.
    #[cfg(feature = "rar")]
    #[serde(default)]
    pub authorization_details: crate::rar::AuthorizationDetails,
    /// The instant this code was MINTED, which is the instant the user's authorization decision
    /// was made. Carried into [`crate::token::IssuedToken::grant_established_at`] on redemption so
    /// that a revocation can tell a code that predates it from one minted afterwards.
    ///
    /// `expires_at` cannot stand in for this: a code minted a minute before a withdrawal expires
    /// minutes AFTER it, so comparing the deadline would let exactly the in-flight redemption a
    /// barrier exists to refuse through.
    ///
    /// `#[serde(default)]`, and the default is the epoch, which is the FAIL-CLOSED direction.
    /// This field is new in 0.9.1, so a code a 0.9.0 node wrote — or is still writing, during a
    /// rolling upgrade — carries no such key, and without a default the read fails outright and
    /// every code that release minted becomes unredeemable the moment this one starts. With it,
    /// the record deserializes and dates from before every barrier that could ever be recorded, so
    /// a standing revocation REFUSES it rather than admitting it. A far-future default would
    /// deserialize just as happily and ADMIT every code 0.9.0 wrote, which is exactly the
    /// resurrection this field exists to close, reintroduced through the upgrade path. The
    /// There is deliberately NO backfill migration: a backfill cannot reach a 0.9.0 node still
    /// writing field-less payloads during a rolling upgrade, which is the window that matters, so
    /// the serde default covers strictly more than one would.
    #[serde(default = "grant_instant_default")]
    pub issued_at: SystemTime,
    /// Expiry instant; the code is dead at and after this instant.
    pub expires_at: SystemTime,
    /// Whether the code has been redeemed, and what it produced.
    pub state: AuthorizationCodeState,
    /// What the host reported about the resource owner's authentication when this code was
    /// approved (the consent-0.8.0 slice; see [`crate::consent::Authentication`]).
    ///
    /// Recorded on the CODE because that is the only path by which the authentication the user
    /// actually performed can reach the token the code mints: the token endpoint has no user in
    /// front of it and cannot ask. Without it, RFC 9470 section 6's `auth_time` and `acr` could
    /// only ever be guessed at.
    #[cfg(feature = "consent")]
    pub authentication: Option<Box<crate::consent::Authentication>>,
}

impl AuthorizationCodeRecord {
    /// A freshly minted, unredeemed code: `state` is [`AuthorizationCodeState::Issued`], because
    /// the `Consumed` form records what a redemption produced and there is nothing to record until
    /// one happens.
    ///
    /// Every argument is a value the record is worthless without, and each is one the RFC names as
    /// the thing a later token request is checked against: the `redirect_uri` it must present again
    /// (RFC 6749 section 4.1.3), the `code_challenge` it must produce a verifier for (RFC 7636
    /// section 4.6), the subject and scope it is redeeming on behalf of, and the instant after
    /// which none of that is true any more. The method is `S256` and not an argument, because
    /// [`CodeChallengeMethod`] has one variant and it is one for a reason.
    #[allow(clippy::too_many_arguments)]
    pub fn new(
        code: impl Into<String>,
        client_id: ClientId,
        redirect_uri: impl Into<String>,
        scope: ScopeSet,
        subject: impl Into<String>,
        code_challenge: impl Into<String>,
        expires_at: SystemTime,
    ) -> Self {
        AuthorizationCodeRecord {
            // FAIL-CLOSED, as `IssuedToken::new` and `RefreshTokenRecord::new` are: a record built
            // by hand has not said when its decision was made, and the epoch predates every
            // revocation, so a standing barrier refuses what it redeems into.
            issued_at: SystemTime::UNIX_EPOCH,
            code: code.into(),
            client_id,
            redirect_uri: redirect_uri.into(),
            // FAIL-CLOSED, like `issued_at` above: a record built by hand has not said whether the
            // authorization request named its redirect URI, and `true` keeps RFC 6749 section
            // 4.1.3's requirement rather than waiving it on a guess.
            redirect_uri_was_explicit: true,
            scope,
            subject: subject.into(),
            code_challenge: code_challenge.into(),
            code_challenge_method: CodeChallengeMethod::S256,
            resource: Vec::new(),
            #[cfg(feature = "rar")]
            authorization_details: crate::rar::AuthorizationDetails::none(),
            expires_at,
            state: AuthorizationCodeState::Issued,
            #[cfg(feature = "consent")]
            authentication: None,
        }
    }
}

/// Hand-written so the one-time `code` never prints (RFC 6749 section 4.1.2 makes it a credential
/// in its own right). EVERY other field prints, on the rule
/// [`crate::token::IssuedToken`]'s `Debug` states in full. `issued_at` in particular: it is what
/// a [`crate::store::RevocationBarrier`] is compared against on redemption, its fail-closed default
/// is the epoch, and without it printing an operator cannot tell a code refused by a standing
/// barrier from one refused for any other reason.
impl fmt::Debug for AuthorizationCodeRecord {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut out = f.debug_struct("AuthorizationCodeRecord");
        out.field("code", &"[redacted]")
            .field("client_id", &self.client_id)
            .field("redirect_uri", &self.redirect_uri)
            .field("redirect_uri_was_explicit", &self.redirect_uri_was_explicit)
            .field("scope", &self.scope)
            .field("subject", &self.subject)
            .field("code_challenge", &self.code_challenge)
            .field("code_challenge_method", &self.code_challenge_method)
            .field("resource", &self.resource);
        // Not a credential: it describes what was authorized, which is precisely what an
        // operator investigating a grant needs to see.
        #[cfg(feature = "rar")]
        out.field("authorization_details", &self.authorization_details);
        out.field("issued_at", &self.issued_at)
            .field("expires_at", &self.expires_at)
            .field("state", &self.state);
        #[cfg(feature = "consent")]
        out.field("authentication", &self.authentication);
        out.finish()
    }
}

/// `?` if the URI has no query yet, `&` if it does.
pub(crate) fn query_separator(uri: &str) -> char {
    if uri.contains('?') {
        '&'
    } else {
        '?'
    }
}

/// Append `name=value` with `value` percent-encoded, advancing the separator after first use.
fn append_param(out: &mut String, sep: &mut char, name: &str, value: &str) {
    out.push(*sep);
    *sep = '&';
    out.push_str(name);
    out.push('=');
    percent_encode_into(out, value);
}

/// Percent-encode everything outside the RFC 3986 unreserved set.
///
/// Deliberately conservative: encoding a character that did not strictly require it is harmless,
/// while failing to encode `&`, `=` or `#` lets a value forge or truncate the query. That is a
/// parameter-injection bug in the one place it must never happen.
fn percent_encode_into(out: &mut String, value: &str) {
    const HEX: &[u8; 16] = b"0123456789ABCDEF";
    for &b in value.as_bytes() {
        if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_' | b'~') {
            out.push(b as char);
        } else {
            out.push('%');
            out.push(HEX[(b >> 4) as usize] as char);
            out.push(HEX[(b & 0x0f) as usize] as char);
        }
    }
}

#[cfg(test)]
#[path = "tests/authorization.rs"]
mod tests;