dniel / traefik-forward-auth0

A backend for performing forward authentication with Auth0 using the Traefik reverse proxy.
GNU General Public License v3.0
85 stars 15 forks source link

Mechanism for SPA to discover auth0 login url #151

Open KarolisL opened 4 years ago

KarolisL commented 4 years ago

SPAs get HTTP 403 after token expires. This works as expected, but it would be nice if in such case, SPA could open a new window with Auth0 login page. For this to work, we need two things:

Relates to https://github.com/dniel/traefik-forward-auth0/issues/128

dniel commented 4 years ago

I havent created a SPA yet for myself, I have focused on API authentication for now. I should probably create a simple SPA to test how forwardauth + auth0 behaves.

ForwardAuth uses the authorization code grant flow from Auth0/OAuth2/OIDC. Maybe even it could be that a SPA should actually authenticate by itself by using implicit grant instead https://auth0.com/docs/architecture-scenarios/spa-api/part-1#implicit-grant and adding the resulting tokens as cookies so that when calling a API, ForwardAuth would authenticate and authorize the request not even knowing that the tokens was not retrieved by the normal authorizatoin code flow.

I need to read up on this to find out if acutally most of the implementation could be done using normal implicit grant flow and possible add eventual missing features to ForwardAuth to support it like the normal authentication code flow for APIs.

dniel commented 4 years ago

New guidance in using implicit grant https://auth0.com/blog/oauth2-implicit-grant-and-spa/

dniel commented 4 years ago

If the API are secured with the same middleware used to enforce web sign on, they are likely to return a 302 when the session cookie expires. 302s aren't really actionable when returned in AJAX calls, and that means that the JS code will need some error management logic to handle the situation- one that possibly doesn't end up sending the browser to pages controlled by an attacker

could possibly be the mechanism you are thinking about.

KarolisL commented 4 years ago

Awesome! This looks exactly what we need: we're serving our SPA from the same domain as our API.

Would you accept a PR if I implemented the following logic: If the call is an API call then forwardauth would:

dniel commented 4 years ago

Hm, wouldn't that be exactly how the standard non-API type of URL is handled? I have to check the code to remember exactly. :)

KarolisL commented 4 years ago

Non-API URLs return HTTP 307 so browser follows them when it does AJAX requests. It might be ok to return HTTP 302 in all cases (API and non-API), but we might break existing API clients which expect 403.

dniel commented 4 years ago

The 307 was chosen so that the method would not be changed by the redirect, but when thinking of it, it is probably not a problem because if you want a redirect you want a redirect with GET to Auth0 login anyways.

KarolisL commented 4 years ago

So what do you think about this approach? I've got another idea that we might return 302 Found for APIs only if client sent some specific header. This way we wouldn't break existing clients which expect 403 Forbidden.

dniel commented 4 years ago

We could try changing for 302 Redirect and see how it behaves, especially for AJAX calls. The 403 forbidden handling of API calls was done to make it cleaner for api clients that anyway cant do anything about the redirects that only a html client can handle. How would the SPA handle the redirect, open a new window to authenticate or do a redirect with the whole page and back?

KarolisL commented 4 years ago

I think SPA could do either of those, it is up to SPA to decide. In my case, I'd prefer if I could just open a new window, make user to get new code from auth0, signin with forwardauth to get new token, and then re-try the request which "failed" with 302.

Does that make sense?

dniel commented 4 years ago

Yup, that was the way I was thinking it would work. Is there a case where a AJAX client would need to distinguish between a redirect for auth, and a normal redirect by the backend API?

KarolisL commented 4 years ago

In my case, there isn't. In most cases I can think of, the backend should use 303 See Other or 307 Temporary Redirect instead of 302.

dniel commented 4 years ago

If you want to have a go at it, create a PR that removes the special response handling of API types, and both API and normal clients get the same 302 redirect. If you have a simple SPA to test with as well, it would be very helpful if you could contribute it for further development for ajax/spa clients. And if you want to use the CI/CD I have configured (have a look at the contribution page) I could add you as a contributor to the repo.

Another question just out of curiosity, what environment do you use? Kubernetes, standalone docker or something else?

man. 27. jan. 2020 kl. 10:59 skrev Karolis Labrencis < notifications@github.com>:

In my case, there isn't. In most cases I can think of, 303 See Other or 307 Temporary Redirect should be used instead of 302.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/dniel/traefik-forward-auth0/issues/151?email_source=notifications&email_token=AAFNMCSHQVTOO5R2HOGJG6TQ72V7RA5CNFSM4KLMFDP2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEJ65WHA#issuecomment-578673436, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAFNMCUBMF6QOKYBODSHAZTQ72V7RANCNFSM4KLMFDPQ .

KarolisL commented 4 years ago

If you want to have a go at it, create a PR that removes the special response handling of API types, and both API and normal clients get the same 302 redirect.

Alright, I hope to start sometime this week.

If you have a simple SPA to test with as well, it would be very helpful if you could contribute it for further development for ajax/spa clients.

I don't have one at the moment, since I'm using my project's SPA to test the flow.

And if you want to use the CI/CD I have configured (have a look at the contribution page) I could add you as a contributor to the repo.

Sure, that would be nice! Also, it would be very helpful if you shared your IntelliJ IDEA code style configuration (I assume you use IDEA to develop this project), since I change half of the lines while hitting "auto format file" with my current code style config. :-)

Another question just out of curiosity, what environment do you use? Kubernetes, standalone docker or something else?

Stable version of forwardauth is deployed to my project's GKE clusters.

When I want to e2e test changes in Forwardauth (i.e. when I'm developing a PR), I use telepresence to swap real ForwardAuth deployment with one started in my IntelliJ IDEA IDE (on my PC). So the traffic flow looks like this: [User browser (uncluding the one on my PC)] ---> [GCP LoadBalancer] ---> [Traefik] ---> [Telepresence proxy in place of Forwardauth pod] ---> ForwardAuth on my PC.

dniel commented 4 years ago

Kool, I added a .editconfig file from my IDEA to the 2.0-rc1 branch. I use default IDEA settings for code style. And also added you as a collaborator to the repo.

dniel commented 4 years ago

@KarolisL the logic for handling authz and authn is implemented as two state machines, https://github.com/dniel/traefik-forward-auth0/blob/2.0-rc1/src/main/kotlin/dniel/forwardauth/domain/authorize/service/AuthorizerStateMachine.kt for authorization and specifically its the lines https://github.com/dniel/traefik-forward-auth0/blob/2.0-rc1/src/main/kotlin/dniel/forwardauth/domain/authorize/service/AuthorizerStateMachine.kt#L176-L180 that handles the special case of if is an Api, then just give access denied or else redirect to auth0 for login.

KarolisL commented 4 years ago

Thanks!

I hope to start next week, since this one is quite busy for me. Regarding SPA, I've found one which, with a bit of modifications, might be good/simple enough to test FordwardAuth: https://github.com/auth0-blog/spa-cookie-demo/tree/with-oidc https://github.com/auth0-blog/spa-cookie-demo/tree/with-oidc

On 2020-01-29, at 11:46, Daniel Engfeldt notifications@github.com wrote:

@KarolisL https://github.com/KarolisL the logic for handling authz and authn is implemented as two state machines, https://github.com/dniel/traefik-forward-auth0/blob/2.0-rc1/src/main/kotlin/dniel/forwardauth/domain/authorize/service/AuthorizerStateMachine.kt https://github.com/dniel/traefik-forward-auth0/blob/2.0-rc1/src/main/kotlin/dniel/forwardauth/domain/authorize/service/AuthorizerStateMachine.kt for authorization and specifically its the lines https://github.com/dniel/traefik-forward-auth0/blob/2.0-rc1/src/main/kotlin/dniel/forwardauth/domain/authorize/service/AuthorizerStateMachine.kt#L176-L180 https://github.com/dniel/traefik-forward-auth0/blob/2.0-rc1/src/main/kotlin/dniel/forwardauth/domain/authorize/service/AuthorizerStateMachine.kt#L176-L180 that handles the special case of if is an Api, then just give access denied or else redirect to auth0 for login.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/dniel/traefik-forward-auth0/issues/151?email_source=notifications&email_token=AAELNDTA2CO4AH6ML4UFXVTRAFF6XA5CNFSM4KLMFDP2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEKGSNZY#issuecomment-579675879, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAELNDSJJTBGAHDYLBLQ3PLRAFF6XANCNFSM4KLMFDPQ.

dniel commented 4 years ago

yeah. agree, that SPA seems like a good start for a SPA for testing

travisghansen commented 4 years ago

I'm interested in following this conversation. I had a similar request for an auth server I author over here: https://github.com/travisghansen/external-auth-server/issues/57

If one has control over the app/SPA I ended up allowing a couple things:

ajax requests currently are determined by:

OR

dniel commented 4 years ago

@travisghansen thanx for your feedback, whats your experience of your solution? are you happy and its working fine or would you solve it in another way now in hindsight?

dniel commented 4 years ago

from what I have thinking is that it would be nice for the client to receive the authentication-url for where to go to do the authentication from the auth-server backend, to be able to redirect the user for authentication in the browser.

dniel commented 4 years ago

what is the content of the realm/scope information you return to the client?

travisghansen commented 4 years ago

@dniel yeah it works great so far. Speaking generally it's main deficiency is you must have control over the SPA which is fine for the use-case but when using 3rd party SPAs you're still kinda stuck. If you control/program/develop the app it's 100% effective.

That's exactly what I do is send the authentication URL to the client both in the Location header (even though the response code is 401) and the WWW-Authenticate header. The relatively tricky part is to make sure the redirect uri sent to the auth provider is not the url requested (from the perspective of the auth server) but rather the url of the SPA endpoint where the user has currently navigated (ie: origin). For scope I just send down what's been configured as the oidc scopes. It's basically useless but perhaps could be of use in some case.

dniel commented 4 years ago

As described in RFC-6750 https://tools.ietf.org/html/rfc6750#section-3 (The OAuth 2.0 Authorization Framework: Bearer Token Usage) the Oauth2 specification has described the proper response from a protected resource server.

  1. Example providing error response with description.

     HTTP/1.1 401 Unauthorized
     WWW-Authenticate: Bearer realm="example",
                       error="invalid_token",
                       error_description="The access token expired"

    other error codes mentioned are invalid_request, invalid_token and insufficient_scope

  2. Example proividing error response of missing scopes.

     scope="openid profile email"
     scope="urn:example:channel=HBO&urn:example:rating=G,PG-13"

    It also says

    All challenges defined by this specification MUST use the auth-scheme value "Bearer". This scheme MUST be followed by one or more auth-param values. The auth-param attributes used or defined by this specification are as follows. Other auth-param attributes MAY be used as well.

A complete error response for a missing scope could be something like.

     HTTP/1.1 401 Unauthorized
     WWW-Authenticate: Bearer realm="app.example.com",
                       error="insufficient_scope",
                       error_description="Missing scope 'whoami:read' to access application."
                       scope="whoami:read"
travisghansen commented 4 years ago

Never read that spec but programmed what I have based on experience and appears to fall in line. I probably should add the error fields though...

dniel commented 4 years ago

Note the phrase Other auth-param attributes MAY be used as well in the spec. I think a possible way to stay as close to the spec could be something like adding an auth_server attribute to the auth-scheme and provide the link to the IDP login page there.

dniel commented 4 years ago

Something like

     HTTP/1.1 401 Unauthorized
     WWW-Authenticate: Bearer realm="app.example.com",
                       error="insufficient_scope",
                       error_description="Missing scope 'whoami:read' to access application."
                       scope="whoami:read"
                       auth_server="https://auth.domain.com/login?redirect=&state="
dniel commented 4 years ago

It also seems that at least the HTTP/1.1 spec (https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.47) is open using multiple WWW-Authenticate headers, and possibly multiple auth-schemas in one header. Auth-Schemas is extendable so another approach could be to create a custom auth-schema to add along with the standard Bearer.

Something like

     HTTP/1.1 401 Unauthorized
     WWW-Authenticate: ForwardAuth realm="app.example.com",
                       auth_server="https://auth.domain.com/login?redirect=&state="
     WWW-Authenticate: Bearer realm="app.example.com",
                       error="insufficient_scope",
                       error_description="Missing scope 'whoami:read' to access application."
                       scope="whoami:read"
travisghansen commented 4 years ago

I guess maybe I’m unclear on what you feel is off spec exactly?

dniel commented 4 years ago

Nothing really :) it seems like both approaches with adding the url to the login server as a custom attribute on the Bearer auth-schema and also the other approach of using a custom auth-schema seems to be perfectly fine in terms of specs.

travisghansen commented 4 years ago

Why not use realm directly? I patterned that after several other services I’ve observed.

dniel commented 4 years ago

do you have an example of syntax?

travisghansen commented 4 years ago

Of the header value or a configuration of the auth server?

dniel commented 4 years ago

From Hypertext Transfer Protocol (HTTP/1.1): Authentication https://tools.ietf.org/html/rfc7235#section-2.2

The "realm" authentication parameter is reserved for use by authentication schemes that wish to indicate a scope of protection.

A protection space is defined by the canonical root URI (the scheme and authority components of the effective request URI; see Section 5.5 of [RFC7230]) of the server being accessed, in combination with the realm value if present. These realms allow the protected resources on a server to be partitioned into a set of protection spaces, each with its own authentication scheme and/or authorization database. The realm value is a string, generally assigned by the origin server, that can have additional semantics specific to the authentication scheme. Note that a response can have multiple challenges with the same auth-scheme but with different realms.

In the mozilla doc the realm is described as https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate

A description of the protected area. If no realm is specified, clients often display a formatted hostname instead.

From what I can see, the realm value should contain a value describing a protected area. But at the same time it can have additional semantics specified by the auth-schema.

dniel commented 4 years ago

Of the header value or a configuration of the auth server?

An example of your realm header value syntax, and also it would be interesting to see a complete example your version of the WWW-Authenticate header.

And if you have any examples of other observed services you mentioned, it would be interesting to see how others has implemented the WWW-Authenticate syntax.

travisghansen commented 4 years ago

I’ll send over sample when back to my desk, but in short it’s the full authorization code flow redirect URI to the provider. ie: login here URL (not to be confused with the redirect_uri query param which tells the auth provider where to return the user after authentication has been completed/attempted)

It’s a slightly different flow with the docker client but general principle applies: https://docs.docker.com/registry/spec/auth/token/

Specifically the realm is not the protected service but rather where authentication can/should happen. Docker may however deviate from the spec..not sure.

travisghansen commented 4 years ago

Regarding the redirect_uri param (what’s really unique here) is that based on how my auth server is configured it will/can dynamically change that depending on if the request looks. If it’s a SPA (ie: ajax request) then the auth server sets the redirect_uri to the page the user requested the resource from vs the endpoint of the resource itself. The general idea being, it doesn’t do much good to redirect the user’s browser back to an api endpoint after successful auth (who wants to look at json or whatever) vs redirecting them back to the page/route that was requesting the api resource.

Not sure if that makes any sense? A bit rambly :)