public AuthCodeGrant CreateAuthorizationCodeGrant(TestOAuth2ServerTokenGenerator generator,
                                                          string[] scopes, string redirectUri, string codeChallenge, OAuth2PkceChallengeMethod codeChallengeMethod)
        {
            string code = generator.CreateAuthorizationCode();

            var grant = new AuthCodeGrant(code, scopes, redirectUri, codeChallenge, codeChallengeMethod);

            AuthGrants.Add(grant);

            return(grant);
        }
        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
            });
        }