Search code examples
c#asp.netoauthowinowin-middleware

OWIN OAuth2 middleware with %23 (# fragment) in redirect uri


I'm using the ASP.NET OWIN/Katana OAuthAuthorizationServer middleware and have run into a snag.

I have some sites trying to get authorization tokens and created an /oauth/authorize endpoint. However, these requests are coming from a SPA (Angular) and will often have a fragment (#) in the redirect URL.

When the request is made, it will set the redirect_uri to that URL, but URL-encoded (so # changes to %23). However, whenever %23 comes across in the URL, the status is always set to 400 and I can't seem to prevent this in any way... Can't override anything and no Web.config reserved bad character changes, etc.

Thus, I tried to just replace it with some placeholder and then redirect back to itself. This worked fine. However, I can't seem to undo my URL change. I need to put # back in the URL and redirect to that, but the OWIN middleware completely ignores any attempts at changing the URL back... Anyone have any ideas?


Solution

  • This is a limitation imposed by RFC 6749 Section 3.1.2:

    The endpoint URI MUST NOT include a fragment component.

    The OpenID Connect specification (and thus the popular IdentityServer3/4), which builds upon the OAuth2 spec, seems not to have this limitation, so you could either switch to OIDC or bend the OAuth2 spec a little bit :)

    In the OWIN middleware the existence of the fragment is checked in OAuthAuthorizationServerHandler.

    A work-around is to bypass this check by replacing the %23 denoting the fragment by a placeholder and patch the location redirect header before it gets send to the browser.

    To replace the incoming %23 with a placeholder, you can override the providers MatchEndpoint method:

    internal class OAuth2AuthorizationServerProvider : OAuthAuthorizationServerProvider
    {
        public override Task MatchEndpoint(OAuthMatchEndpointContext context)
        {
            if (context.Request.Path.StartsWithSegments(context.Options.AuthorizeEndpointPath)
                && context.Request.QueryString.HasValue)
            {
                context.Request.QueryString = new QueryString(
                    context.Request.QueryString.Value.Replace("%23", "__fragment__"));
            }
    
            return base.MatchEndpoint(context);
        }
    }
    

    The other part is trickier as you can't seem to do it by default from the provider class because the redirectUri is kept internally on the OAuthValidateClientRedirectUriContext.

    A hacky way is to use reflection and modify this RedirectUri in the providers ValidateClientRedirectUri method:

    public override async Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
    {
        // Test if the redirect uri is allowed, if not call context.SetError("invalid_client");
    
        var redirectUri = context.RedirectUri.Replace("__fragment__", "#");
        var setter = context.GetType().GetProperty(nameof(context.RedirectUri))?.GetSetMethod(true);
        setter?.Invoke(context, new[] { redirectUri });
        context.Validated(redirectUri);
    }
    

    Another way is to add a simple middleware to the chain (before the OAuth2 middleware) that monitors the response (so after awaiting the next middleware) and patches the Location header where needed. This is less tricky than setting a private property, but now the patch is spread over 2 different places which is less maintainable.

    With above work-around a redirect URI like

    https://server.com/#spa/path/123
    

    note that you will pass a URL-encoded version to the authorize endpoint redirect_uri param

    will result in a redirect to

    https://server.com/#spa/path/123&access_token=<the_access_token>