Skip to content

Definition of the redirect_uri endpoint #240

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
jgoux opened this issue Dec 18, 2018 · 9 comments
Closed

Definition of the redirect_uri endpoint #240

jgoux opened this issue Dec 18, 2018 · 9 comments

Comments

@jgoux
Copy link

jgoux commented Dec 18, 2018

Environment
  • lua-resty-openidc version : 1.7.0
  • OpenID Connect provider : Keycloak

Hello,

First I'd like to thank you for this very useful library which is a game changer for me in my architecture! 👏

I'm pretty new to Openid so I have a few questions about how lua-resty-openidc handles the Authorization flow so I can be sure I'm on the right tracks!

The unclear point to me is the redirect_uri, I have seen no example of a location block definition of a redirect_uri endpoint, is that normal? By reading the code I saw that calling require("resty.openidc").authenticate(opts) in such endpoint would be enough to lua-resty-openidc to take care of the rest (getting the tokens, making the session, and get the userinfo if the option is provided)?
I already created my client on Keycloak (named "proxy") with a valid redirect uri (http://auth.mydomain.com/callback)

I intend to use lua-resty-openidc in conjonction with nginx to make an authentication proxy to selectively protect my apps in virtualhost blocks, here is a simplified conf that I'd like to set up (
I'll probably extract the whole access_by_lua_block into its own module as it's always the same block) :

# A public app
server {
    listen 80;
    server_name public.mydomain.com;
    location / {
      proxy_pass http://localhost:3000;
    }
}

# A private app
server {
    listen 80;
    server_name private1.mydomain.com;
    location / {
      access_by_lua_block {
        local opts = {
          discovery = "http://keycloak/auth/realms/my-realms/.well-known/openid-configuration",
          client_id = "proxy",
          client_secret = "...",
          redirect_uri = "http://auth.mydomain.com/callback"
        }
        local res, err = require("resty.openidc").authenticate(opts)
        if err then
          ngx.status = 500
          ngx.say(err)
          ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
        end
        ngx.req.set_header("X-USER", res.id_token.sub)
      }
    proxy_pass http://localhost:4000;
  }
}

# Another private app
server {
    listen 80;
    server_name private2.mydomain.com;
    location / {
      access_by_lua_block {
        local opts = {
          discovery = "http://keycloak/auth/realms/my-realms/.well-known/openid-configuration",
          client_id = "proxy",
          client_secret = "...",
          redirect_uri = "http://auth.mydomain.com/callback"
        }
        local res, err = require("resty.openidc").authenticate(opts)
        if err then
          ngx.status = 500
          ngx.say(err)
          ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
        end
        ngx.req.set_header("X-USER", res.id_token.sub)
      }
    proxy_pass http://localhost:4001;
    }
}

# The "redirect_uri" block, do I need it?
# If I need it, what should I precisely do after the require("resty.openidc").authenticate call?
server {
    listen 80;
    server_name auth.mydomain.com
    location /callback {
      access_by_lua_block {
        local opts = {
          discovery = "http://keycloak/auth/realms/my-realms/.well-known/openid-configuration",
          client_id = "proxy",
          client_secret = "...",
          redirect_uri = "http://auth.mydomain.com/callback"
        }
        local res, err = require("resty.openidc").authenticate(opts)
        if err then
          ngx.status = 500
          ngx.say(err)
          ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
        end
      }
    }    
}

I'll gladly add this example in the wiki once I'm sure I'm not making a big mistake somewhere. 😄

My second question is how can I control the session expiration set by lua-resty-openidc according to my token expiration?
I'd expect the session to be deleted as soon as the access_token is expired, and only renewed if there is a refresh_token and the renew_access_token_on_expiry is true.
I tried setting the renew_access_token_on_expiry to false but my page was still accessible even if the session state is :

# This should delete the session as token_expired=true and opts.renew_access_token_on_expiry=false
session.present=true, session.data.id_token=true, session.data.authenticated=true, opts.force_reauthorize=nil, opts.renew_access_token_on_expiry=false, try_to_renew=false, token_expired=true

Have a nice day!

@bodewig
Copy link
Collaborator

bodewig commented Dec 19, 2018

The "normal" way of using lua-resty-openidc is a setup, where the configured redirect_uri is convered by the location that spans the URI space you want to protect, i.e. it would be something like https://private2.mydomain.com/callback in your case. This is why you don't see any explicit locations for redirect_uri in the examples.

It doesn't have to be used that way, though. A setup where redirect URI is not part of the protected URI space is perfectly possible but will only work if all of your blocks have access to the same session - which in general means you need to configure Cookies to use mydomain.com as Cookie domain. An unauthenticated request to private?.mydomain.com will create a session that holds the OAuth2 state and the original target URL and you /callback must evetually be able to see the Cookie (and decrypt it which means all your servers need to use the same secret). In additon the target URL will be stored as a relative URL by default. you will have to explicitly pass in the absolute URI of the current request as second parameter to authenticate in your private opts.

@jgoux
Copy link
Author

jgoux commented Dec 19, 2018

You totally understand the architecture I'm trying to achieve! 🌟

I'd like to use a single callback living only on nginx so all my apps don't have to worry about the auth flow at all.

And as you noticed, they'll all live under the same domain so cookie sharing was where I wanted to go. (actually they'll live under sub-subdomain so I hope cookie sharing is still possible using the primary domain as Cookie domain)

Is it possible to configure a cookie domain with lua-resty-openidc API?

In my auth.mydomain.com location block, do I need to do anything other than what I currently do in my example to implement the "callback" uri?

Thank you so much for the indications!

EDIT : Just saw the session_opts parameter, I guess it answers my first question! 😄

@bodewig
Copy link
Collaborator

bodewig commented Dec 19, 2018

I happen to know an installation that does something similar to what you are trying to achieve :-)

For the configuration of the session cookie you need to look into configuring lua-resty-session - see https://github.com/bungle/lua-resty-session

I don't think there is anything special you need to configure apart from that, personally I'd recommend to set accept_unsupported_alg to false and restrict token_signing_alg_values_expected to what you expect to use (likely RSA only).

@jgoux
Copy link
Author

jgoux commented Dec 20, 2018

@bodewig thanks for the advices!

I was able to set the domain using authenticate() fourth argument!

local session_opts = { cookie = { domain = ".mydomain.com" } }

Do you have any info about this part?

My second question is how can I control the session expiration set by lua-resty-openidc according to my token expiration?
I'd expect the session to be deleted as soon as the access_token is expired, and only renewed if there is a refresh_token and the renew_access_token_on_expiry is true.
I tried setting the renew_access_token_on_expiry to false but my page was still accessible even if the session state is :

# This should delete the session as token_expired=true and opts.renew_access_token_on_expiry=false
session.present=true, session.data.id_token=true, session.data.authenticated=true, opts.force_reauthorize=nil, opts.renew_access_token_on_expiry=false, try_to_renew=false, token_expired=true

@bodewig
Copy link
Collaborator

bodewig commented Dec 20, 2018

Sorry, I somehow overlooked the second part of your question.

The session duration can be configured independently and you should probably make it match your token's expiration time. lua-resty-openidc does not offer a way to do that automatically.

The user would be redirected to the OP if

  • there is no session at all
  • there is a session and lua-resty-openidc knows the user has never been authenticated successfully

in your case lua-resty-openidc has an active session and knows the user has authenticated at one point in time and it knows the token has expired. In this case the access is allowed, which seems a little unexpected. /cc @zandbelt

@zandbelt
Copy link
Contributor

About the expired access token: that is a common misunderstanding about OpenID Connect. The application session at the OpenID Connect RP is independent of the tokens that it receives. Those tokens are:

  • an id_token that represents and authentication event which bootstraps the application session; the expiration of that token is to time-constrain its usage to bootstrap application session(s) at the RP
  • an access_token that is used to call out to APIs including the userinfo endpoint at the OP; the lifetime of that token is to time-constrain the ability to call APIs with it
    In case of lua-resty-openidc the application session is controlled by the lua-resty-session settings, thus via the session.cookie.renew and session.cookie. lifetime.

In case the access token expires during the lifetime of the session, it may be renewed if needed for calling APIs but it does not impact the application's session lifetime, by design.

@jgoux
Copy link
Author

jgoux commented Dec 21, 2018

@zandbelt Thank you so much for clearing things up!

I'll just play with session.cookie.renew and session.cookie.lifetime to manage my user's session.

I learnt so much about openid with this project. You are a wonderful community! 🎆

@jgoux jgoux closed this as completed Dec 21, 2018
@jgoux
Copy link
Author

jgoux commented Dec 22, 2018

I have one last question about the workflow. 😅

Once the session is validated and the user authenticated, in which form do you forward the user’s infos to the end services? (in my case they're mostly nodejs web apps serving SPA frontends using React)

As far as I understand, the encrypted cookie session is only used by lua-resty-openidc to carry the tokens infos and the session state, so reading from it in my end services would not be a good idea (and they would all need the session secret). Also I don't need to encrypt my user infos as there is nothing sensible in it, it's just the basics identity informations plus a set of roles.

I was thinking about serializing the userinfo result and put it inside a http header like this :
ngx.header['X-USER'] = ngx.encode_base64(require("cjson").encode(res.user))

Or maybe I should make a second cookie to carry that information?

What do you think? Do you have any recommendations about the “post-validation session” part?

I know it's a little off topic but as you both have experience with this workflow I didn't find a better place to ask.

Thanks for your help!

@zandbelt
Copy link
Contributor

please use the mailing list for this type of questions

passing info to the backend is basically up to you; an example can be found in the README.md:

          -- set headers with user info: this will overwrite any existing headers
          -- but also scrub(!) them in case no value is provided in the token
          ngx.req.set_header("X-USER", res.id_token.sub)

passing the info to the frontend i.e. SPAs is a different topic: you can use response headers for claims/tokens or create a separate endpoint to return that information

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants