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.");
            }
        }
예제 #3
0
        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);
        }
예제 #4
0
        public void ToByteStringTest()
        {
            var baseNString = "eg==";
            var expected    = new byte[] { 122 };

            var actual = Base64UrlConvert.ToBytes(baseNString);

            Assert.Equal(expected, actual);
        }
예제 #5
0
        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);
        }
예제 #6
0
        public void ToStringTest()
        {
            var expected = "eg==";
            var data     = new byte[] { 122 };

            var actual = Base64UrlConvert.ToString(data);

            Assert.Equal(expected, actual);
        }
예제 #7
0
        public void ToBytesCharArrayTest()
        {
            var baseNChars = "eg==".ToCharArray();
            var expected   = new byte[] { 122 };

            var actual = Base64UrlConvert.ToBytes(baseNChars);

            Assert.Equal(expected, actual);
        }
예제 #8
0
        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);
        }
예제 #9
0
        public void ToCharArrayTest()
        {
            var expected = "eg==".ToCharArray();
            var data     = new byte[] { 122 };

            var actual = Base64UrlConvert.ToCharArray(data);

            Assert.Equal(expected, actual);
        }
예제 #10
0
        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);
        }
예제 #11
0
        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);
        }
예제 #12
0
        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
            });
        }