using System.Buffers.Text; using System.Security.Cryptography; using System.Text; namespace DodoSSH.Client.Auth; /// /// A PKCE code verifier and its S256 challenge. See RFC 7636. /// /// /// /// A public client cannot keep a secret, so PKCE is what stops an authorization code being /// redeemed by anyone who intercepts it — on a loopback redirect that means any other local /// process that manages to receive the callback. The token endpoint only accepts the code /// alongside the verifier whose hash it saw at authorization time. /// /// /// S256 only. The plain method is still in the RFC and offers no protection /// whatsoever against an attacker who saw the authorization request. /// /// public sealed class PkcePair { /// The challenge method sent to the authorization endpoint. public const string Method = "S256"; /// /// Entropy behind the verifier. 32 bytes renders as 43 base64url characters, the RFC's /// minimum length and comfortably beyond guessing. /// private const int VerifierEntropyBytes = 32; private PkcePair(string codeVerifier, string codeChallenge) { CodeVerifier = codeVerifier; CodeChallenge = codeChallenge; } /// The secret held until the token exchange. Never leaves the process. public string CodeVerifier { get; } /// The hash sent with the authorization request. public string CodeChallenge { get; } /// Generates a fresh pair. public static PkcePair Create() { // Base64url of random bytes, which satisfies the RFC's unreserved-character set without // any escaping. Generating characters directly from an alphabet would be one more place to // get a modulo bias wrong for no benefit. var verifier = Base64Url.EncodeToString(RandomNumberGenerator.GetBytes(VerifierEntropyBytes)); var challenge = Base64Url.EncodeToString(SHA256.HashData(Encoding.ASCII.GetBytes(verifier))); return new PkcePair(verifier, challenge); } }