AspNetKatana
AspNetKatana copied to clipboard
How to handle response_type=form_post in OAuthAuthorizationServerProvider.AuthorizationEndpointResponse
Hello,
I'm trying to implement the OAuth authorize endpoint including support for the response_type=form_post
.
By default the OAuthAuthorizationServerHandler.ApplyResponseGrantAsync
method sends a redirect HTTP response, with a redirect location that is the configured OAuthAuthorizationServerOptions.FormPostEndpoint
. According to https://docs.microsoft.com/en-us/previous-versions/aspnet/mt180788(v=vs.113) this endpoint is responsible to send the HTML to the client/browser that will post back the authorize response data (i.e. the authorization code or access code, depending on the flow, plus additional data; https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html). The OAuthAuthorizationServerHandler.ApplyResponseGrantAsync
method sends this redirect just after calling the IOAuthAuthorizationServerProvider.AuthorizationEndpointResponse
.
Why is the OAuthAuthorizationServerHandler.ApplyResponseGrantAsync
sending this redirect, instead of sending a form post response immediately?
I would like to prevent this redirect to the FormPostEndPoint
and send the form post HTML immediately to the client/browser IOAuthAuthorizationServerProvider.AuthorizationEndpointResponse
. I can write the required HTML body to OAuthAuthorizationEndpointResponseContext.Response
, but the redirect is still happening. Calling the context.RequestCompleted()
does not help, because OAuthAuthorizationServerHandler.ApplyResponseGrantAsync
ignores this. The redirect response sent to the client does contain the body, but is still a 302 response including a location header, rather than a 200 response.
My implementation of AuthorizationEndpointResponse
is currently
public override Task AuthorizationEndpointResponse(OAuthAuthorizationEndpointResponseContext context)
{
NameValueCollection _returnParams = HttpUtility.ParseQueryString(string.Empty);
foreach (KeyValuePair<string, object> _parameter in context.AdditionalResponseParameters)
{
_returnParams[_parameter.Key] = _parameter.Value.ToString();
}
_returnParams[IdentityProviderConstants.Parameters.Code] = context.AuthorizationCode;
if (!string.IsNullOrEmpty(context.AuthorizeEndpointRequest.State))
{
_returnParams[IdentityProviderConstants.Parameters.State] = context.AuthorizeEndpointRequest.State;
}
if (context.AuthorizeEndpointRequest.IsFormPostResponseMode)
{
StringBuilder _inputFields = new StringBuilder();
foreach (string _returnParam in _returnParams.AllKeys)
{
foreach (string _value in _returnParams.GetValues(_returnParam))
{
_inputFields.AppendLine($@"<input type=""hidden"" name=""{_returnParam}"" value=""{_value}""/>");
}
}
StringBuilder _responseBody = new StringBuilder(
$@"
<html>
<head><title>Submit This Form</title></head>
<body onload=""javascript:document.forms[0].submit()"">
<form method=""post"" action=""{context.AuthorizeEndpointRequest.RedirectUri}"">
{_inputFields}
</form>
</body>
</html>");
context.Response.Write(_responseBody.ToString());
context.RequestCompleted();
}
else
{
UriBuilder _redirectUri = new UriBuilder(context.AuthorizeEndpointRequest.RedirectUri);
_redirectUri.Query = _returnParams.ToString();
context.Response.Redirect(_redirectUri.Uri.AbsoluteUri);
context.RequestCompleted();
}
return Task.CompletedTask;
}
Any feedback is appreciated.
These OAuthAuthorizationServer components are outdated and we do not recommend using them anymore. There are better community implementations available like IdentityServer, or you can use a hosted solution like Azure Active Directory.
Thank you for your response @Tratcher.
Replacing the OAuthAuthorizationServer components at this point is not really a cost effective solution. Beside the work, a component like IdentityServer is also no longer maintained as open source / free software. Maybe OpenIddict will be a candidate when updating our software to .NET 6.0.
For now I have found a workaround using the IOwinResponse.OnSendingHeaders
.
public override Task AuthorizationEndpointResponse(OAuthAuthorizationEndpointResponseContext context)
{
Uri _uri = new Uri(context.AuthorizeEndpointRequest.RedirectUri);
NameValueCollection _returnParams = HttpUtility.ParseQueryString(_uri.Query);
foreach (KeyValuePair<string, object> _parameter in context.AdditionalResponseParameters)
{
_returnParams[_parameter.Key] = _parameter.Value.ToString();
}
_returnParams[IdentityProviderConstants.Parameters.Code] = context.AuthorizationCode;
if (!string.IsNullOrEmpty(context.AuthorizeEndpointRequest.State))
{
_returnParams[IdentityProviderConstants.Parameters.State] = context.AuthorizeEndpointRequest.State;
}
// If 'response_mode=form_post' was send, handle returning the appropriate response here; see
// https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html. We need to handle it here,
// because the caller of this method (`OAuthAuthorizationServerHandler.ApplyResponseGrantAsync`)
// will issue a redirect to `context.Options.FormPostEndpoint`, which we want to prevent. For
// other response modes let the caller handle the response.
if (context.AuthorizeEndpointRequest.IsFormPostResponseMode)
{
StringBuilder _inputFields = new StringBuilder();
foreach (string _returnParam in _returnParams.AllKeys)
{
foreach (string _value in _returnParams.GetValues(_returnParam) ?? Array.Empty<string>())
{
_inputFields.AppendLine($@"<input type=""hidden"" name=""{_returnParam}"" value=""{_value}""/>");
}
}
StringBuilder _responseBody = new StringBuilder(
$@"
<html>
<head><title>Submit This Form</title></head>
<body onload=""javascript:document.forms[0].submit()"">
<form method=""post"" action=""{context.AuthorizeEndpointRequest.RedirectUri}"">
{_inputFields.ToString().TrimEnd()}
</form>
</body>
</html>");
context.Response.Write(_responseBody.ToString());
context.Response.OnSendingHeaders(
state =>
{
if (state is OAuthAuthorizationEndpointResponseContext _context)
{
// Undo the 'redirect' set by the caller of `AuthorizationEndpointResponse`, i.e.
// `OAuthAuthorizationServerHandler.ApplyResponseGrantAsync`.
_context.Response.StatusCode = 200;
_context.Response.Headers.Remove("Location");
}
},
context);
context.RequestCompleted();
}
return Task.CompletedTask;
}
Maybe OpenIddict will be a candidate when updating our software to .NET 6.0.
It's worth noting OpenIddict is now natively compatible with Microsoft.Owin
(support was added in 3.0 to allow OAuthAuthorizationServerMiddleware
and OpenIdConnectServerMiddleware
users to modernize their OAuth 2.0/OIDC implementation without being forced to migrate to ASP.NET Core).