Skip to content

Commit 2ab87fd

Browse files
Add PKCE verification
1 parent 8ad8023 commit 2ab87fd

File tree

4 files changed

+34
-5
lines changed

4 files changed

+34
-5
lines changed

BlazorWasmOsmOauth/Infrastructure/LocalStorageService.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ public sealed class LocalStorageService(IJSRuntime jsRuntime)
1919
/// </summary>
2020
public const string OsmStateKey = "osm-state";
2121

22+
/// <summary>
23+
/// The localstorage key for the osm pkce value, used to prevent MITM attacks
24+
/// See: https://developer.okta.com/blog/2019/05/01/is-the-oauth-implicit-flow-dead#does-the-authorization-code-flow-make-browser-based-apps-totally-secure
25+
/// </summary>
26+
public const string OsmPkceKey = "osm-pkce";
27+
2228
/// <summary>
2329
/// Gets the specified value from localstorage
2430
/// </summary>

BlazorWasmOsmOauth/Layout/MainLayout.razor.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Security.Cryptography;
12
using BlazorWasmOsmOauth.Infrastructure;
23
using BlazorWasmOsmOauth.Models;
34

@@ -14,12 +15,19 @@ public partial class MainLayout(AppConfig config, NavigationManager navManager,
1415
LocalStorageService localStorageService) : LayoutComponentBase
1516
{
1617
private string SourceRevisionId => config.SourceRevisionId;
17-
18+
1819
private async Task GoToLogin()
1920
{
2021
Guid state = Guid.NewGuid();
2122
await localStorageService.SetItemAsync(LocalStorageService.OsmStateKey, state, CancellationToken.None);
2223

24+
Guid pkce1 = Guid.NewGuid();
25+
Guid pkce2 = Guid.NewGuid();
26+
string pkce = pkce1.ToString("N") + pkce2.ToString("N");
27+
await localStorageService.SetItemAsync(LocalStorageService.OsmPkceKey, pkce, CancellationToken.None);
28+
29+
byte[] pkceSha256 = SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(pkce));
30+
2331
navManager.NavigateTo(
2432
navManager.GetUriWithQueryParameters($"{config.OsmAuthBaseUrl}/oauth2/authorize",
2533
new Dictionary<string, object?>
@@ -28,7 +36,9 @@ private async Task GoToLogin()
2836
{ "client_id", config.ClientId },
2937
{ "redirect_uri", config.RedirectUri },
3038
{ "scope", "read_prefs" },
31-
{ "state", state }
39+
{ "state", state },
40+
{ "code_challenge", Convert.ToBase64String(pkceSha256).Replace('+', '-').Replace('/', '_').TrimEnd('=') },
41+
{ "code_challenge_method", "S256" }
3242
}.AsReadOnly()
3343
)
3444
);

BlazorWasmOsmOauth/Osm/OsmApiClient.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,13 @@ public class OsmApiClient(IHttpClientFactory httpClientFactory)
2929
/// Get the user token from the OSM API, using the code from the OAuth2 flow. Or null if the request fails.
3030
/// </summary>
3131
/// <returns></returns>
32-
public async Task<TokenResponse?> GetTokenAsync(string code, string redirectUri, string clientId)
32+
public async Task<TokenResponse?> GetTokenAsync(string code, string redirectUri, string clientId, string pkce)
3333
{
3434
string queryStr = "grant_type=authorization_code"
3535
+ $"&code={HttpUtility.UrlEncode(code)}"
3636
+ $"&redirect_uri={HttpUtility.UrlEncode(redirectUri)}"
37-
+ $"&client_id={HttpUtility.UrlEncode(clientId)}";
37+
+ $"&client_id={HttpUtility.UrlEncode(clientId)}"
38+
+ $"&code_verifier={HttpUtility.UrlEncode(pkce)}";
3839

3940
HttpResponseMessage response = await _authClient.PostAsync($"/oauth2/token?{queryStr}", new StringContent(string.Empty, Encoding.UTF8, "application/x-www-form-urlencoded"));
4041

BlazorWasmOsmOauth/Pages/AuthCompletePage.razor.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,19 @@ private async Task GetToken()
6363
return;
6464
}
6565

66-
TokenResponse? tokenResp = await osmClient.GetTokenAsync(Code, config.RedirectUri, config.ClientId);
66+
string? pkce = await localStorageService.GetItemAsync<string>(LocalStorageService.OsmPkceKey, CancellationToken.None);
67+
68+
if (string.IsNullOrWhiteSpace(pkce))
69+
{
70+
// Wait for the dialog to close, then redirect to the home page
71+
IDialogReference dialogReference = await dialogService.ShowErrorAsync("The PKCE value is missing.", title: "Missing PKCE");
72+
await dialogReference.Result;
73+
74+
navManager.NavigateTo("/");
75+
return;
76+
}
77+
78+
TokenResponse? tokenResp = await osmClient.GetTokenAsync(Code, config.RedirectUri, config.ClientId, pkce);
6779

6880
if (tokenResp != null)
6981
{

0 commit comments

Comments
 (0)