public string CreatePkceCodeVerifier() { // // RFC 7636 requires the code verifier to match the following ABNF: // // code-verifier = 43*128unreserved // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" // ALPHA = %x41-5A / %x61-7A // DIGIT = %x30-39 // // The base64url encoding (RFC 6749) is a subset of these characters // so we opt to convert our random bytes into Base64URL format for // the code verifier. // // RFC 7636 mandates the code verifier must be between 43 and 128 characters // long (inclusive). We want to generate a string at the top end of this range // for maximum entropy. At the same time we want to avoid using the padding // character '=' because this character is percent-encoded when used in URLs. // To avoid padding we need the number of input bytes to be divisible by 3. // // In order to achieve 128 base64url characters AND avoid padding we should // generate exactly 96 random bytes. Why 96 bytes? 96 is divisible by 3 and: // // 96 bytes -> 768 bits -> 128 base64url characters (6 bits per character) // var buf = new byte[96]; var rng = RandomNumberGenerator.Create(); rng.GetBytes(buf); return(Base64UrlConvert.Encode(buf, PkceIncludeBase64UrlPadding)); }
public string CreatePkceCodeChallenge(OAuth2PkceChallengeMethod challengeMethod, string codeVerifier) { switch (challengeMethod) { case OAuth2PkceChallengeMethod.Plain: return(codeVerifier); case OAuth2PkceChallengeMethod.Sha256: // The "S256" code challenge is computed as follows, per RFC 7636: // // code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) // using (var sha256 = SHA256.Create()) { return(Base64UrlConvert.Encode( sha256.ComputeHash( Encoding.ASCII.GetBytes(codeVerifier) ), PkceIncludeBase64UrlPadding )); } default: throw new ArgumentOutOfRangeException(nameof(challengeMethod), challengeMethod, "Unknown PKCE code challenge method."); } }
public void ToBytesByteArrayPartialTest() { var baseNCharBytes = "99eg==99".ToCharArray().Select(ch => (byte)ch).ToArray(); var expected = new byte[] { 122 }; var actual = Base64UrlConvert.ToBytes(baseNCharBytes, 2, baseNCharBytes.Length - 4); Assert.Equal(expected, actual); }
public void ToByteStringTest() { var baseNString = "eg=="; var expected = new byte[] { 122 }; var actual = Base64UrlConvert.ToBytes(baseNString); Assert.Equal(expected, actual); }
public void ToBytesByteArrayTest() { var baseNCharBytes = "eg==".ToCharArray().Select(ch => (byte)ch).ToArray(); var expected = new byte[] { 122 }; var actual = Base64UrlConvert.ToBytes(baseNCharBytes); Assert.Equal(expected, actual); }
public void ToStringTest() { var expected = "eg=="; var data = new byte[] { 122 }; var actual = Base64UrlConvert.ToString(data); Assert.Equal(expected, actual); }
public void ToBytesCharArrayTest() { var baseNChars = "eg==".ToCharArray(); var expected = new byte[] { 122 }; var actual = Base64UrlConvert.ToBytes(baseNChars); Assert.Equal(expected, actual); }
public void ToCharArrayPartialTest() { var expected = "eg==".ToCharArray(); var data = new byte[] { 255, 122, 255 }; var actual = Base64UrlConvert.ToCharArray(data, 1, 1); Assert.Equal(expected, actual); }
public void ToCharArrayTest() { var expected = "eg==".ToCharArray(); var data = new byte[] { 122 }; var actual = Base64UrlConvert.ToCharArray(data); Assert.Equal(expected, actual); }
public void ToBytesStringPartialTest() { var baseNString = "99eg==99"; var expected = new byte[] { 122 }; var actual = Base64UrlConvert.ToBytes(baseNString, 2, baseNString.Length - 4); Assert.Equal(expected, actual); }
public void OAuth2CryptographicCodeGenerator_CreatePkceCodeChallenge_Sha256_ReturnsBase64UrlEncodedSha256HashOfAsciiVerifier() { var generator = new OAuth2CryptographicCodeGenerator(); var verifier = generator.CreatePkceCodeVerifier(); byte[] verifierAsciiBytes = Encoding.ASCII.GetBytes(verifier); byte[] hashedBytes; using (var sha256 = SHA256.Create()) { hashedBytes = sha256.ComputeHash(verifierAsciiBytes); } var expectedChallenge = Base64UrlConvert.Encode(hashedBytes, false); var actualChallenge = generator.CreatePkceCodeChallenge(OAuth2PkceChallengeMethod.Sha256, verifier); Assert.Equal(expectedChallenge, actualChallenge); }
public void Base64UrlConvert_Encode_WithoutPadding(byte[] data, string expected) { string actual = Base64UrlConvert.Encode(data, includePadding: false); Assert.Equal(expected, actual); }
public TokenEndpointResponseJson CreateTokenByAuthorizationGrant( TestOAuth2ServerTokenGenerator generator, string authCode, string codeVerifier, string redirectUri) { var grant = AuthGrants.FirstOrDefault(x => x.Code == authCode); if (grant is null) { throw new Exception($"Invalid authorization code '{authCode}'"); } // Validate the grant's code challenge was constructed from the given code verifier if (!string.IsNullOrWhiteSpace(grant.CodeChallenge)) { if (string.IsNullOrWhiteSpace(codeVerifier)) { throw new Exception("Missing code verifier"); } switch (grant.CodeChallengeMethod) { case OAuth2PkceChallengeMethod.Sha256: using (var sha256 = SHA256.Create()) { string challenge = Base64UrlConvert.Encode( sha256.ComputeHash( Encoding.ASCII.GetBytes(codeVerifier) ), false ); if (challenge != grant.CodeChallenge) { throw new Exception($"Invalid code verifier '{codeVerifier}'"); } } break; case OAuth2PkceChallengeMethod.Plain: // The case matters! if (!StringComparer.Ordinal.Equals(codeVerifier, grant.CodeChallenge)) { throw new Exception($"Invalid code verifier '{codeVerifier}'"); } break; } } // If an explicit redirect URI was used as part of the authorization request then // the redirect URI used for the token call must match exactly. if (!string.IsNullOrWhiteSpace(grant.RedirectUri) && !StringComparer.Ordinal.Equals(grant.RedirectUri, redirectUri)) { throw new Exception("Redirect URI must match exactly the one used when requesting the authorization code."); } string accessToken = generator.CreateAccessToken(); string refreshToken = generator.CreateRefreshToken(); // Remove the auth code grant now we've generated an access token (do not allow auth code reuse) AuthGrants.Remove(grant); // Store the tokens AccessTokens[accessToken] = refreshToken; RefreshTokens[refreshToken] = grant.Scopes; return(new TokenEndpointResponseJson { TokenType = Constants.Http.WwwAuthenticateBearerScheme, AccessToken = accessToken, RefreshToken = refreshToken, Scope = string.Join(" ", grant.Scopes) // Keep the same scopes as before }); }