public async Task ClaimTokenAsync(string accountLinkingToken, string tenantId, string userId, string codeVerifier)
    {
        var(acctState, exp) = await _oAuthStateService.GetAsync(accountLinkingToken);

        // ensure the PKCE verifier is correct
        var codeGuess = Pkce.Base64UrlEncodeSha256(codeVerifier);

        if (!string.Equals(acctState.CodeChallenge, codeGuess, StringComparison.OrdinalIgnoreCase))
        {
            _logger.LogWarning("PKCE verification failed:\nChallenge:{codeChallenge}\nVerifier:{verifier}\nH(Verifier):{guess}", acctState.CodeChallenge, codeVerifier, codeGuess);
            throw new Exception("PKCE code verification failed");
        }

        // ensure this isn't a replay
        await _replayValidator.ClaimIdAsync(acctState.Id, exp);

        // Claim the oauth code from the downstream
        var oAuthResult = await _oAuthServiceClient.ClaimCodeAsync(acctState.OAuthCode);

        var dto = new OAuthUserTokenDto
        {
            AccessToken           = oAuthResult.AccessToken,
            AccessTokenExpiration = DateTimeOffset.Now + TimeSpan.FromSeconds(oAuthResult.ExpiresInSeconds),
            RefreshToken          = oAuthResult.RefreshToken
        };

        var serializedDto = JsonSerializer.Serialize(dto);

        await _userTokenStore.SetTokenAsync(tenantId : tenantId, userId : userId, serializedDto);
    }
    public async Task <AccessTokenResultBase> GetAccessTokenAsync(string tenantId, string userId)
    {
        var tokenDtoString = await _userTokenStore.GetTokenAsync(tenantId, userId);

        if (tokenDtoString == default)
        {
            _logger.LogInformation("Underlying store contained no token, returning null");
            return(GetNeedsConsentResult());
        }

        var tokenDto = JsonSerializer.Deserialize <OAuthUserTokenDto>(tokenDtoString);

        if (tokenDto == default)
        {
            _logger.LogWarning("Token stored was valid json, but not valid DTO! Did the schema change?");
            return(GetNeedsConsentResult());
        }

        _logger.LogInformation(
            "Token expired? [{isExpired}]: [{timeDelta}]",
            DateTime.Now >= tokenDto.AccessTokenExpiration,
            DateTime.Now - tokenDto.AccessTokenExpiration);

        // If the 'cached' token is still valid don't do the refresh.
        if (tokenDto.AccessTokenExpiration >= DateTime.Now)
        {
            return(new AccessTokenResult
            {
                AccessToken = tokenDto.AccessToken,
            });
        }

        _logger.LogInformation("Performing oAuth refresh flow");
        var jsonBody = await _oAuthServiceClient.RefreshAccessTokenAsync(tokenDto.RefreshToken);

        if (jsonBody == default)
        {
            return(GetNeedsConsentResult());
        }

        string accessToken       = jsonBody.AccessToken != "" ? jsonBody.AccessToken : tokenDto.AccessToken;
        long   expirationSeconds = jsonBody.ExpiresInSeconds;
        // If we get a refresh token in the response, we need to replace the refresh token. Otherwise re-use the current refresh token.
        string nextRefreshToken = jsonBody.RefreshToken ?? tokenDto.RefreshToken;

        var dto = new OAuthUserTokenDto
        {
            AccessToken           = accessToken,
            AccessTokenExpiration = DateTimeOffset.Now + TimeSpan.FromSeconds(expirationSeconds),
            RefreshToken          = nextRefreshToken
        };

        var serializedDto = JsonSerializer.Serialize(dto);
        await _userTokenStore.SetTokenAsync(
            tenantId : tenantId,
            userId : userId,
            token : serializedDto);

        return(new AccessTokenResult
        {
            AccessToken = accessToken,
        });
    }