From 55dabd98004bf5ef5d5d209036d3ac523cbf5e46 Mon Sep 17 00:00:00 2001 From: Joe DeCock Date: Tue, 2 Apr 2024 10:18:17 -0500 Subject: [PATCH 01/12] Initial implementation of PAR --- src/OidcClient/AuthorizeClient.cs | 55 +++++++++++++++++++++++++-- src/OidcClient/OidcClient.cs | 3 +- src/OidcClient/OidcClientOptions.cs | 7 +++- src/OidcClient/ProviderInformation.cs | 8 ++++ 4 files changed, 67 insertions(+), 6 deletions(-) diff --git a/src/OidcClient/AuthorizeClient.cs b/src/OidcClient/AuthorizeClient.cs index 3eee319..10748ff 100644 --- a/src/OidcClient/AuthorizeClient.cs +++ b/src/OidcClient/AuthorizeClient.cs @@ -8,7 +8,6 @@ using IdentityModel.OidcClient.Results; using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -44,7 +43,7 @@ public async Task AuthorizeAsync(AuthorizeRequest request, AuthorizeResult result = new AuthorizeResult { - State = CreateAuthorizeState(request.ExtraParameters) + State = await CreateAuthorizeStateAsync(request.ExtraParameters) }; var browserOptions = new BrowserOptions(result.State.StartUrl, _options.RedirectUri) @@ -86,7 +85,8 @@ public async Task EndSessionAsync(LogoutRequest request, return await _options.Browser.InvokeAsync(browserOptions, cancellationToken); } - public AuthorizeState CreateAuthorizeState(Parameters frontChannelParameters) + public async Task CreateAuthorizeStateAsync(Parameters frontChannelParameters + ) { _logger.LogTrace("CreateAuthorizeStateAsync"); @@ -99,13 +99,46 @@ public AuthorizeState CreateAuthorizeState(Parameters frontChannelParameters) CodeVerifier = pkce.CodeVerifier, }; - state.StartUrl = CreateAuthorizeUrl(state.State, pkce.CodeChallenge, frontChannelParameters); + if(_options.UsePushedAuthorization) + { + var parResponse = await PushAuthorizationRequestAsync(state.State, pkce.CodeChallenge, frontChannelParameters); + if(parResponse.IsError) + { + // TODO - Consider logging more information (but we would need to sanitize?) + _logger.LogError("Failed to push authorization parameters"); + + // TODO - Consider how to signal errors to the caller/which exception type to throw + throw new InvalidOperationException(parResponse.Error); + } + state.StartUrl = CreateAuthorizeUrl(parResponse.RequestUri, _options.ClientId); + } + else + { + state.StartUrl = CreateAuthorizeUrl(state.State, pkce.CodeChallenge, frontChannelParameters); + } _logger.LogDebug(LogSerializer.Serialize(state)); return state; } + private async Task PushAuthorizationRequestAsync(string state, string codeChallenge, Parameters frontChannelParameters) + { + var http = _options.CreateClient(); + var par = new PushedAuthorizationRequest + { + Address = _options.ProviderInformation.PushedAuthorizationRequestEndpoint, + ClientId = _options.ClientId, + + ClientSecret = _options.ClientSecret, + ClientAssertion = _options.ClientAssertion, + + Parameters = CreateAuthorizeParameters(state, codeChallenge, frontChannelParameters), + }; + + return await http.PushAuthorizationAsync(par); + } + internal string CreateAuthorizeUrl(string state, string codeChallenge, Parameters frontChannelParameters) { @@ -117,6 +150,20 @@ internal string CreateAuthorizeUrl(string state, string codeChallenge, return request.Create(parameters); } + internal string CreateAuthorizeUrl(string requestUri, string clientId) + { + _logger.LogTrace("CreateAuthorizeUrl with requestUri from PAR"); + + var parameters = new Parameters + { + { OidcConstants.AuthorizeRequest.ClientId, clientId }, + { OidcConstants.AuthorizeRequest.RequestUri, requestUri } + }; + var request = new RequestUrl(_options.ProviderInformation.AuthorizeEndpoint); + + return request.Create(parameters); + } + internal string CreateEndSessionUrl(string endpoint, LogoutRequest request) { _logger.LogTrace("CreateEndSessionUrl"); diff --git a/src/OidcClient/OidcClient.cs b/src/OidcClient/OidcClient.cs index 20b4f33..c51d07a 100644 --- a/src/OidcClient/OidcClient.cs +++ b/src/OidcClient/OidcClient.cs @@ -109,7 +109,7 @@ public virtual async Task PrepareLoginAsync(Parameters frontChan _logger.LogTrace("PrepareLoginAsync"); await EnsureConfigurationAsync(cancellationToken); - return _authorizeClient.CreateAuthorizeState(frontChannelParameters); + return await _authorizeClient.CreateAuthorizeStateAsync(frontChannelParameters); } /// @@ -428,6 +428,7 @@ internal async Task EnsureProviderInformationAsync(CancellationToken cancellatio KeySet = disco.KeySet, AuthorizeEndpoint = disco.AuthorizeEndpoint, + PushedAuthorizationRequestEndpoint = disco.PushedAuthorizationRequestEndpoint, TokenEndpoint = disco.TokenEndpoint, EndSessionEndpoint = disco.EndSessionEndpoint, UserInfoEndpoint = disco.UserInfoEndpoint, diff --git a/src/OidcClient/OidcClientOptions.cs b/src/OidcClient/OidcClientOptions.cs index c488428..3e539da 100644 --- a/src/OidcClient/OidcClientOptions.cs +++ b/src/OidcClient/OidcClientOptions.cs @@ -165,7 +165,7 @@ public class OidcClientOptions public HttpMessageHandler RefreshTokenInnerHttpHandler { get; set; } /// - /// Gets or sets the HTTP handler used for back-channel communication (token and userinfo endpoint). + /// Gets or sets the HTTP handler used for back-channel communication (token, pushed authorization, and userinfo endpoints). /// /// /// The backchannel handler. @@ -243,5 +243,10 @@ public class OidcClientOptions JwtClaimTypes.AccessTokenHash, JwtClaimTypes.StateHash }; + + /// + /// Gets or sets a flag to enable Pushed Authorization Requests (PAR). + /// + public bool UsePushedAuthorization { get; set; } } } \ No newline at end of file diff --git a/src/OidcClient/ProviderInformation.cs b/src/OidcClient/ProviderInformation.cs index 73d02d4..e16d0fc 100644 --- a/src/OidcClient/ProviderInformation.cs +++ b/src/OidcClient/ProviderInformation.cs @@ -44,6 +44,14 @@ public class ProviderInformation /// public string AuthorizeEndpoint { get; set; } + /// + /// Gets or sets the pushed authorization request (PAR) endpoint. + /// + /// + /// The PAR endpoint. + /// + public string PushedAuthorizationRequestEndpoint { get; set; } + /// /// Gets or sets the end session endpoint. /// From 2a7e411a9b9eb21ea9e0aee9d09a8f1066320ff2 Mon Sep 17 00:00:00 2001 From: Joe DeCock Date: Tue, 2 Apr 2024 10:20:23 -0500 Subject: [PATCH 02/12] Use PAR in sample clients --- .gitignore | 6 ++- .vscode/launch.json | 42 +++++++++++++++++++ .vscode/tasks.json | 29 +++++++++++++ .../ConsoleClientWithBrowser.csproj | 2 +- clients/ConsoleClientWithBrowser/Program.cs | 4 +- .../ConsoleClientWithBrowserAndDPoP.csproj | 2 +- .../Program.cs | 1 + 7 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json diff --git a/.gitignore b/.gitignore index fb8d610..7be99fd 100644 --- a/.gitignore +++ b/.gitignore @@ -205,4 +205,8 @@ tools/ .idea # Visual Studio Code workspace options -.vscode \ No newline at end of file +.vscode/settings.json + +# Tokens stored by sample clients +clients/*/proofkey +clients/*/refresh_token diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..40c84b0 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,42 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "ConsoleClientWithBrowser", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build-ConsoleClientWithBrowser", + "program": "${workspaceFolder}/clients/ConsoleClientWithBrowser/bin/Debug/net8.0/ConsoleClientWithBrowser.dll", + "args": [], + "cwd": "${workspaceFolder}/clients/ConsoleClientWithBrowser", + "serverReadyAction": { + "action": "openExternally", + "pattern": "\\bNow listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "console": "externalTerminal" + }, + { + "name": "ConsoleClientWithBrowserAndDPoP", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build-ConsoleClientWithBrowserAndDPoP", + "program": "${workspaceFolder}/clients/ConsoleClientWithBrowserAndDPoP/bin/Debug/net8.0/ConsoleClientWithBrowserAndDPoP.dll", + "args": [], + "cwd": "${workspaceFolder}/clients/ConsoleClientWithBrowserAndDPoP", + "serverReadyAction": { + "action": "openExternally", + "pattern": "\\bNow listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "console": "externalTerminal" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..504d3d0 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,29 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build-ConsoleClientWithBrowser", + "type": "process", + "command": "dotnet", + "args": [ + "build", + "${workspaceFolder}/clients/ConsoleClientWithBrowser/ConsoleClientWithBrowser.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "build-ConsoleClientWithBrowserAndDPoP", + "type": "process", + "command": "dotnet", + "args": [ + "build", + "${workspaceFolder}/clients/ConsoleClientWithBrowserAndDPoP/ConsoleClientWithBrowserAndDPoP.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/clients/ConsoleClientWithBrowser/ConsoleClientWithBrowser.csproj b/clients/ConsoleClientWithBrowser/ConsoleClientWithBrowser.csproj index 67a5d52..9648a6c 100644 --- a/clients/ConsoleClientWithBrowser/ConsoleClientWithBrowser.csproj +++ b/clients/ConsoleClientWithBrowser/ConsoleClientWithBrowser.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 Exe diff --git a/clients/ConsoleClientWithBrowser/Program.cs b/clients/ConsoleClientWithBrowser/Program.cs index b93751f..1e5d70e 100644 --- a/clients/ConsoleClientWithBrowser/Program.cs +++ b/clients/ConsoleClientWithBrowser/Program.cs @@ -46,7 +46,9 @@ private static async Task SignIn() Browser = browser, IdentityTokenValidator = new JwtHandlerIdentityTokenValidator(), - RefreshTokenInnerHttpHandler = new SocketsHttpHandler() + RefreshTokenInnerHttpHandler = new SocketsHttpHandler(), + + UsePushedAuthorization = true }; var serilog = new LoggerConfiguration() diff --git a/clients/ConsoleClientWithBrowserAndDPoP/ConsoleClientWithBrowserAndDPoP.csproj b/clients/ConsoleClientWithBrowserAndDPoP/ConsoleClientWithBrowserAndDPoP.csproj index e95b8de..b7fc863 100644 --- a/clients/ConsoleClientWithBrowserAndDPoP/ConsoleClientWithBrowserAndDPoP.csproj +++ b/clients/ConsoleClientWithBrowserAndDPoP/ConsoleClientWithBrowserAndDPoP.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 Exe diff --git a/clients/ConsoleClientWithBrowserAndDPoP/Program.cs b/clients/ConsoleClientWithBrowserAndDPoP/Program.cs index 60926fa..367ce3f 100644 --- a/clients/ConsoleClientWithBrowserAndDPoP/Program.cs +++ b/clients/ConsoleClientWithBrowserAndDPoP/Program.cs @@ -48,6 +48,7 @@ private static async Task SignIn() Scope = "openid profile api offline_access", FilterClaims = false, Browser = browser, + UsePushedAuthorization = true }; options.ConfigureDPoP(proofKey); From ef52d0c70a68972a3d3be72832004f17644dbe58 Mon Sep 17 00:00:00 2001 From: Joe DeCock Date: Tue, 2 Apr 2024 10:22:20 -0500 Subject: [PATCH 03/12] Minor whitespace correction --- src/OidcClient/AuthorizeClient.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/OidcClient/AuthorizeClient.cs b/src/OidcClient/AuthorizeClient.cs index 10748ff..725f1e8 100644 --- a/src/OidcClient/AuthorizeClient.cs +++ b/src/OidcClient/AuthorizeClient.cs @@ -85,8 +85,7 @@ public async Task EndSessionAsync(LogoutRequest request, return await _options.Browser.InvokeAsync(browserOptions, cancellationToken); } - public async Task CreateAuthorizeStateAsync(Parameters frontChannelParameters - ) + public async Task CreateAuthorizeStateAsync(Parameters frontChannelParameters) { _logger.LogTrace("CreateAuthorizeStateAsync"); From 5523cafa1c5109d2c0b167fadc8b935479cc1038 Mon Sep 17 00:00:00 2001 From: Joe DeCock Date: Wed, 3 Apr 2024 14:29:48 -0500 Subject: [PATCH 04/12] Minor formatting of proj file --- src/DPoP/DPoP.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/DPoP/DPoP.csproj b/src/DPoP/DPoP.csproj index 61aa617..4a85bca 100644 --- a/src/DPoP/DPoP.csproj +++ b/src/DPoP/DPoP.csproj @@ -7,7 +7,7 @@ netstandard2.0;net6.0 latest - enable + enable OAuth2;OAuth 2.0;OpenID Connect;Security;Identity;IdentityServer;DPoP DPoP extensions for IdentityModel.OidcClient @@ -39,7 +39,7 @@ - + @@ -52,7 +52,7 @@ - + \ No newline at end of file From 0a22f860c38d4481b51eec6b10782e6996d5c333 Mon Sep 17 00:00:00 2001 From: Joe DeCock Date: Wed, 3 Apr 2024 14:30:38 -0500 Subject: [PATCH 05/12] Add error message to AuthorizeState If PAR fails, we now surface that failure in the same way that other response types do. --- src/OidcClient/AuthorizeClient.cs | 13 ++++++++++--- src/OidcClient/AuthorizeState.cs | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/OidcClient/AuthorizeClient.cs b/src/OidcClient/AuthorizeClient.cs index 725f1e8..ff4f09b 100644 --- a/src/OidcClient/AuthorizeClient.cs +++ b/src/OidcClient/AuthorizeClient.cs @@ -46,6 +46,13 @@ public async Task AuthorizeAsync(AuthorizeRequest request, State = await CreateAuthorizeStateAsync(request.ExtraParameters) }; + if(result.State.IsError) + { + result.Error = result.State.Error; + result.ErrorDescription = result.State.ErrorDescription; + return result; + } + var browserOptions = new BrowserOptions(result.State.StartUrl, _options.RedirectUri) { Timeout = TimeSpan.FromSeconds(request.Timeout), @@ -103,11 +110,11 @@ public async Task CreateAuthorizeStateAsync(Parameters frontChan var parResponse = await PushAuthorizationRequestAsync(state.State, pkce.CodeChallenge, frontChannelParameters); if(parResponse.IsError) { - // TODO - Consider logging more information (but we would need to sanitize?) _logger.LogError("Failed to push authorization parameters"); - // TODO - Consider how to signal errors to the caller/which exception type to throw - throw new InvalidOperationException(parResponse.Error); + state.Error = "Failed to push authorization parameters"; + state.ErrorDescription = parResponse.Error; + return state; } state.StartUrl = CreateAuthorizeUrl(parResponse.RequestUri, _options.ClientId); } diff --git a/src/OidcClient/AuthorizeState.cs b/src/OidcClient/AuthorizeState.cs index f3f5ec2..541be71 100644 --- a/src/OidcClient/AuthorizeState.cs +++ b/src/OidcClient/AuthorizeState.cs @@ -7,7 +7,7 @@ namespace IdentityModel.OidcClient /// /// Represents the state the needs to be hold between starting the authorize request and the response /// - public class AuthorizeState + public class AuthorizeState : Result { /// /// Gets or sets the start URL. From c99f19b5f381c095305b30a01bbadab701d5e773 Mon Sep 17 00:00:00 2001 From: Joe DeCock Date: Wed, 3 Apr 2024 14:31:05 -0500 Subject: [PATCH 06/12] Clean up test clients - Remove defunct ConsoleClient - Add a manual mode console client --- .vscode/launch.json | 19 +- .vscode/tasks.json | 12 ++ clients/ConsoleClient/ConsoleClient.csproj | 18 -- clients/ConsoleClient/Program.cs | 173 ------------------ .../ManualModeConsoleClient.csproj | 14 ++ clients/ManualModeConsoleClient/Program.cs | 109 +++++++++++ 6 files changed, 153 insertions(+), 192 deletions(-) delete mode 100644 clients/ConsoleClient/ConsoleClient.csproj delete mode 100644 clients/ConsoleClient/Program.cs create mode 100644 clients/ManualModeConsoleClient/ManualModeConsoleClient.csproj create mode 100644 clients/ManualModeConsoleClient/Program.cs diff --git a/.vscode/launch.json b/.vscode/launch.json index 40c84b0..65a7459 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -37,6 +37,23 @@ "ASPNETCORE_ENVIRONMENT": "Development" }, "console": "externalTerminal" - } + }, + { + "name": "ManualModeConsoleClient", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build-ManualModeConsoleClient", + "program": "${workspaceFolder}/clients/ManualModeConsoleClient/bin/Debug/net8.0-windows/ManualModeConsoleClient.dll", + "args": [], + "cwd": "${workspaceFolder}/clients/ManualModeConsoleClient", + "serverReadyAction": { + "action": "openExternally", + "pattern": "\\bNow listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "console": "externalTerminal" + }, ] } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 504d3d0..99e8cf0 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -24,6 +24,18 @@ "/consoleloggerparameters:NoSummary" ], "problemMatcher": "$msCompile" + }, + { + "label": "build-ManualModeConsoleClient", + "type": "process", + "command": "dotnet", + "args": [ + "build", + "${workspaceFolder}/clients/ManualModeConsoleClient/ManualModeConsoleClient.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" } ] } \ No newline at end of file diff --git a/clients/ConsoleClient/ConsoleClient.csproj b/clients/ConsoleClient/ConsoleClient.csproj deleted file mode 100644 index 8aa6b48..0000000 --- a/clients/ConsoleClient/ConsoleClient.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - netcoreapp2.1 - Exe - - - - - - - - - - - - - diff --git a/clients/ConsoleClient/Program.cs b/clients/ConsoleClient/Program.cs deleted file mode 100644 index 57d16e8..0000000 --- a/clients/ConsoleClient/Program.cs +++ /dev/null @@ -1,173 +0,0 @@ -using IdentityModel.OidcClient; -using Microsoft.Net.Http.Server; -using Newtonsoft.Json; -using Serilog; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; - -namespace ConsoleClient -{ - public class Program - { - static string _authority = "https://demo.identityserver.io"; - static string _api = "https://api.identityserver.io/identity"; - - public static void Main(string[] args) => MainAsync().GetAwaiter().GetResult(); - - public static async Task MainAsync() - { - Console.WriteLine("+-----------------------+"); - Console.WriteLine("| Sign in with OIDC |"); - Console.WriteLine("+-----------------------+"); - Console.WriteLine(""); - Console.WriteLine("Press any key to sign in..."); - Console.ReadKey(); - - await SignInAsync(); - - Console.ReadKey(); - } - - private async static Task SignInAsync() - { - // create a redirect URI using an available port on the loopback address. - string redirectUri = string.Format("http://127.0.0.1:7890/"); - - // create an HttpListener to listen for requests on that redirect URI. - var settings = new WebListenerSettings(); - settings.UrlPrefixes.Add(redirectUri); - var http = new WebListener(settings); - - http.Start(); - Console.WriteLine("Listening.."); - - var options = new OidcClientOptions - { - Authority = _authority, - ClientId = "interactive.public", - RedirectUri = redirectUri, - Scope = "openid profile api", - FilterClaims = true, - LoadProfile = true - }; - - var serilog = new LoggerConfiguration() - .MinimumLevel.Verbose() - .Enrich.FromLogContext() - .WriteTo.LiterateConsole(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message}{NewLine}{Exception}{NewLine}") - .CreateLogger(); - - options.LoggerFactory.AddSerilog(serilog); - - var client = new OidcClient(options); - var state = await client.PrepareLoginAsync(); - - OpenBrowser(state.StartUrl); - - var context = await http.AcceptAsync(); - var formData = GetRequestPostData(context.Request); - - if (formData == null) - { - Console.WriteLine("Invalid response"); - return; - } - - await SendResponseAsync(context.Response); - - var result = await client.ProcessResponseAsync(formData, state); - - ShowResult(result); - } - - private static void ShowResult(LoginResult result) - { - if (result.IsError) - { - Console.WriteLine("\n\nError:\n{0}", result.Error); - return; - } - - Console.WriteLine("\n\nClaims:"); - foreach (var claim in result.User.Claims) - { - Console.WriteLine("{0}: {1}", claim.Type, claim.Value); - } - - Console.WriteLine($"\nidentity token: {result.IdentityToken}"); - Console.WriteLine($"access token: {result.AccessToken}"); - Console.WriteLine($"refresh token: {result?.RefreshToken ?? "none"}"); - - var values = JsonConvert.DeserializeObject>(result.TokenResponse.Raw); - - Console.WriteLine($"Raw TokenResponse ..."); - foreach (var item in values) - { - Console.WriteLine($"{item.Key}: {item.Value}"); - } - - } - - private static async Task SendResponseAsync(Response response) - { - string responseString = $"Please return to the app."; - var buffer = Encoding.UTF8.GetBytes(responseString); - - response.ContentLength = buffer.Length; - - var responseOutput = response.Body; - await responseOutput.WriteAsync(buffer, 0, buffer.Length); - responseOutput.Flush(); - } - - public static void OpenBrowser(string url) - { - try - { - Process.Start(url); - } - catch - { - // hack because of this: https://github.com/dotnet/corefx/issues/10361 - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - url = url.Replace("&", "^&"); - Process.Start(new ProcessStartInfo("cmd", $"/c start {url}") { CreateNoWindow = true }); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - Process.Start("xdg-open", url); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - Process.Start("open", url); - } - else - { - throw; - } - } - } - - public static string GetRequestPostData(Request request) - { - if (!request.HasEntityBody) - { - return null; - } - - using (var body = request.Body) - { - using (var reader = new StreamReader(body)) - { - return reader.ReadToEnd(); - } - } - } - } -} diff --git a/clients/ManualModeConsoleClient/ManualModeConsoleClient.csproj b/clients/ManualModeConsoleClient/ManualModeConsoleClient.csproj new file mode 100644 index 0000000..cdee441 --- /dev/null +++ b/clients/ManualModeConsoleClient/ManualModeConsoleClient.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0-windows + enable + enable + + + + + + + diff --git a/clients/ManualModeConsoleClient/Program.cs b/clients/ManualModeConsoleClient/Program.cs new file mode 100644 index 0000000..64588b4 --- /dev/null +++ b/clients/ManualModeConsoleClient/Program.cs @@ -0,0 +1,109 @@ +using IdentityModel.OidcClient; +using System.Diagnostics; +using System.Net; +using System.Runtime.InteropServices; +using System.Text; + +Console.WriteLine("+-----------------------+"); +Console.WriteLine("| Sign in with OIDC |"); +Console.WriteLine("+-----------------------+"); +Console.WriteLine(""); +Console.WriteLine("Press any key to sign in..."); +Console.ReadKey(); + +SignIn(); + +Console.ReadKey(); + +async void SignIn() +{ + // create a redirect URI using an available port on the loopback address. + string redirectUri = string.Format("http://127.0.0.1:7890/"); + Console.WriteLine("redirect URI: " + redirectUri); + + // create an HttpListener to listen for requests on that redirect URI. + var http = new HttpListener(); + http.Prefixes.Add(redirectUri); + Console.WriteLine("Listening.."); + http.Start(); + + var options = new OidcClientOptions + { + Authority = "https://demo.duendesoftware.com", + ClientId = "interactive.public", + Scope = "openid profile api", + RedirectUri = redirectUri, + }; + + var client = new OidcClient(options); + var state = await client.PrepareLoginAsync(); + + if(state.IsError) + { + Console.WriteLine($"Failed to create authentication state: {state.Error} - {state.ErrorDescription}"); + http.Stop(); + return; + } + + Console.WriteLine($"Start URL: {state.StartUrl}"); + + // open system browser to start authentication + Process.Start(new ProcessStartInfo + { + FileName = state.StartUrl, + UseShellExecute = true, + }); + + // wait for the authorization response. + var context = await http.GetContextAsync(); + + // sends an HTTP response to the browser. + var response = context.Response; + string responseString = string.Format("Please return to the app."); + var buffer = Encoding.UTF8.GetBytes(responseString); + response.ContentLength64 = buffer.Length; + var responseOutput = response.OutputStream; + await responseOutput.WriteAsync(buffer, 0, buffer.Length); + responseOutput.Close(); + + var result = await client.ProcessResponseAsync(context.Request.RawUrl, state); + + BringConsoleToFront(); + + if (result.IsError) + { + Console.WriteLine("\n\nError:\n{0}", result.Error); + } + else + { + Console.WriteLine("\n\nClaims:"); + foreach (var claim in result.User.Claims) + { + Console.WriteLine("{0}: {1}", claim.Type, claim.Value); + } + + Console.WriteLine(); + Console.WriteLine("Access token:\n{0}", result.AccessToken); + + if (!string.IsNullOrWhiteSpace(result.RefreshToken)) + { + Console.WriteLine("Refresh token:\n{0}", result.RefreshToken); + } + } + + http.Stop(); +} + +// Hack to bring the Console window to front. +// ref: http://stackoverflow.com/a/12066376 +[DllImport("kernel32.dll", ExactSpelling = true)] +static extern IntPtr GetConsoleWindow(); + +[DllImport("user32.dll")] +[return: MarshalAs(UnmanagedType.Bool)] +static extern bool SetForegroundWindow(IntPtr hWnd); + +void BringConsoleToFront() +{ + SetForegroundWindow(GetConsoleWindow()); +} From 1887bfada0b6c32a6c651f801cb08c9fc4911994 Mon Sep 17 00:00:00 2001 From: Joe DeCock Date: Wed, 3 Apr 2024 15:51:14 -0500 Subject: [PATCH 07/12] Enable PAR by default when it is supported by the provider --- clients/ConsoleClientWithBrowser/Program.cs | 2 -- clients/ConsoleClientWithBrowserAndDPoP/Program.cs | 3 +-- src/OidcClient/AuthorizeClient.cs | 4 +++- src/OidcClient/OidcClientOptions.cs | 6 ++++-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/clients/ConsoleClientWithBrowser/Program.cs b/clients/ConsoleClientWithBrowser/Program.cs index 1e5d70e..1e5eb4e 100644 --- a/clients/ConsoleClientWithBrowser/Program.cs +++ b/clients/ConsoleClientWithBrowser/Program.cs @@ -47,8 +47,6 @@ private static async Task SignIn() Browser = browser, IdentityTokenValidator = new JwtHandlerIdentityTokenValidator(), RefreshTokenInnerHttpHandler = new SocketsHttpHandler(), - - UsePushedAuthorization = true }; var serilog = new LoggerConfiguration() diff --git a/clients/ConsoleClientWithBrowserAndDPoP/Program.cs b/clients/ConsoleClientWithBrowserAndDPoP/Program.cs index 367ce3f..c3f82ea 100644 --- a/clients/ConsoleClientWithBrowserAndDPoP/Program.cs +++ b/clients/ConsoleClientWithBrowserAndDPoP/Program.cs @@ -47,8 +47,7 @@ private static async Task SignIn() RedirectUri = redirectUri, Scope = "openid profile api offline_access", FilterClaims = false, - Browser = browser, - UsePushedAuthorization = true + Browser = browser }; options.ConfigureDPoP(proofKey); diff --git a/src/OidcClient/AuthorizeClient.cs b/src/OidcClient/AuthorizeClient.cs index ff4f09b..900d808 100644 --- a/src/OidcClient/AuthorizeClient.cs +++ b/src/OidcClient/AuthorizeClient.cs @@ -105,8 +105,10 @@ public async Task CreateAuthorizeStateAsync(Parameters frontChan CodeVerifier = pkce.CodeVerifier, }; - if(_options.UsePushedAuthorization) + if(_options.ProviderInformation.PushedAuthorizationRequestEndpoint.IsPresent() && + !_options.DisablePushedAuthorization) { + _logger.LogDebug("The IdentityProvider contains a pushed authorization request endpoint. Automatically pushing authorization parameters. Use DisablePushedAuthorization to opt out."); var parResponse = await PushAuthorizationRequestAsync(state.State, pkce.CodeChallenge, frontChannelParameters); if(parResponse.IsError) { diff --git a/src/OidcClient/OidcClientOptions.cs b/src/OidcClient/OidcClientOptions.cs index 3e539da..97428cb 100644 --- a/src/OidcClient/OidcClientOptions.cs +++ b/src/OidcClient/OidcClientOptions.cs @@ -245,8 +245,10 @@ public class OidcClientOptions }; /// - /// Gets or sets a flag to enable Pushed Authorization Requests (PAR). + /// Gets or sets a flag to disable Pushed Authorization Requests (PAR). + /// By default, we use PAR when there is a configured PAR endpoint or + /// when the discovery endpoint indicates that it supports PAR. /// - public bool UsePushedAuthorization { get; set; } + public bool DisablePushedAuthorization { get; set; } = false; } } \ No newline at end of file From 86e9a908c52ef0cfd2c840e49c47a03b984de836 Mon Sep 17 00:00:00 2001 From: Joe DeCock Date: Wed, 3 Apr 2024 15:53:53 -0500 Subject: [PATCH 08/12] Fix up solution file --- IdentityModel.OidcClient.sln | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/IdentityModel.OidcClient.sln b/IdentityModel.OidcClient.sln index ac54fb3..9d397e4 100644 --- a/IdentityModel.OidcClient.sln +++ b/IdentityModel.OidcClient.sln @@ -27,6 +27,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleClientWithBrowserAnd EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TrimmableAnalysis", "test\TrimmableAnalysis\TrimmableAnalysis.csproj", "{672FE4E9-9071-4C59-95FC-F265DF6B2FF5}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManualModeConsoleClient", "clients\ManualModeConsoleClient\ManualModeConsoleClient.csproj", "{864F9320-43C3-494E-946A-B19B4A7D1CA3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -69,6 +71,10 @@ Global {672FE4E9-9071-4C59-95FC-F265DF6B2FF5}.Debug|Any CPU.Build.0 = Debug|Any CPU {672FE4E9-9071-4C59-95FC-F265DF6B2FF5}.Release|Any CPU.ActiveCfg = Release|Any CPU {672FE4E9-9071-4C59-95FC-F265DF6B2FF5}.Release|Any CPU.Build.0 = Release|Any CPU + {864F9320-43C3-494E-946A-B19B4A7D1CA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {864F9320-43C3-494E-946A-B19B4A7D1CA3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {864F9320-43C3-494E-946A-B19B4A7D1CA3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {864F9320-43C3-494E-946A-B19B4A7D1CA3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -83,6 +89,7 @@ Global {0E1807AF-4142-4A3D-925C-BBA019E4E777} = {3DEB81D4-5B40-4D20-AC50-66D1CD6EA24A} {7DDDA872-49C0-43F0-8B88-2531BF828DDE} = {A4154BEB-4B4A-4A48-B75D-B52432304F36} {672FE4E9-9071-4C59-95FC-F265DF6B2FF5} = {3DEB81D4-5B40-4D20-AC50-66D1CD6EA24A} + {864F9320-43C3-494E-946A-B19B4A7D1CA3} = {A4154BEB-4B4A-4A48-B75D-B52432304F36} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {66951C2E-691F-408C-9283-F2455F390A9A} From 6d1cc9bb711f8a9fc56407ad6eb9f0ade731895d Mon Sep 17 00:00:00 2001 From: Joe DeCock Date: Wed, 3 Apr 2024 15:54:47 -0500 Subject: [PATCH 09/12] Add github readme to solution --- IdentityModel.OidcClient.sln | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/IdentityModel.OidcClient.sln b/IdentityModel.OidcClient.sln index 9d397e4..eb7f499 100644 --- a/IdentityModel.OidcClient.sln +++ b/IdentityModel.OidcClient.sln @@ -29,6 +29,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TrimmableAnalysis", "test\T EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManualModeConsoleClient", "clients\ManualModeConsoleClient\ManualModeConsoleClient.csproj", "{864F9320-43C3-494E-946A-B19B4A7D1CA3}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EB34682-F5A0-4F91-A624-64F042B5B910}" + ProjectSection(SolutionItems) = preProject + README.md = README.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU From 75b1360482b9575ba59baf72a52ae1edfc05f727 Mon Sep 17 00:00:00 2001 From: Joe DeCock Date: Wed, 3 Apr 2024 16:02:00 -0500 Subject: [PATCH 10/12] Use GetClientAssertionAsync in PAR --- src/OidcClient/AuthorizeClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OidcClient/AuthorizeClient.cs b/src/OidcClient/AuthorizeClient.cs index 900d808..bd0b840 100644 --- a/src/OidcClient/AuthorizeClient.cs +++ b/src/OidcClient/AuthorizeClient.cs @@ -139,7 +139,7 @@ private async Task PushAuthorizationRequestAsync(st ClientId = _options.ClientId, ClientSecret = _options.ClientSecret, - ClientAssertion = _options.ClientAssertion, + ClientAssertion = await _options.GetClientAssertionAsync(), Parameters = CreateAuthorizeParameters(state, codeChallenge, frontChannelParameters), }; From 20e77ec750d85b55d691155521b19feb84a0300c Mon Sep 17 00:00:00 2001 From: Joe DeCock Date: Wed, 3 Apr 2024 16:06:59 -0500 Subject: [PATCH 11/12] Suppress strong name warning in dpop test proj We don't care about strong naming in general, and we definitely don't care in a test project --- test/DPoPTests/DPoPTests.csproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/DPoPTests/DPoPTests.csproj b/test/DPoPTests/DPoPTests.csproj index 56e309a..1e1d336 100644 --- a/test/DPoPTests/DPoPTests.csproj +++ b/test/DPoPTests/DPoPTests.csproj @@ -10,6 +10,10 @@ true + + CS8002 + + From e948bbe92ebf02ae0b387a7097d8832e69dabb04 Mon Sep 17 00:00:00 2001 From: Joe DeCock Date: Wed, 3 Apr 2024 20:59:45 -0500 Subject: [PATCH 12/12] Swap error and error description --- src/OidcClient/AuthorizeClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OidcClient/AuthorizeClient.cs b/src/OidcClient/AuthorizeClient.cs index bd0b840..098f0b1 100644 --- a/src/OidcClient/AuthorizeClient.cs +++ b/src/OidcClient/AuthorizeClient.cs @@ -114,8 +114,8 @@ public async Task CreateAuthorizeStateAsync(Parameters frontChan { _logger.LogError("Failed to push authorization parameters"); - state.Error = "Failed to push authorization parameters"; - state.ErrorDescription = parResponse.Error; + state.Error = parResponse.Error; + state.ErrorDescription = "Failed to push authorization parameters"; return state; } state.StartUrl = CreateAuthorizeUrl(parResponse.RequestUri, _options.ClientId);