OK, I got it working enough to authenticate the user and get a
userinfo dict, but it doesn't have the all-important roles
information. I may need to add some scopes; I'm asking the server
admin about that. The original problem was I received an incomplete
authorization URL, '/auth' instead of
'auth/realms/REALM/protocol/openid-connect/auth'. Here's my code
(paraphrased) and a bunch more questions:
======
import pprint
import secrets
import requests_oauthlib
# Utilities
def get_oauth2_session(request, state):
redirect_uri = request.route_url("login")
client = request.registry.settings["oauth2.client"]
scope = request.registry.settings["oauth2.scope"] # scope == None.
oauth = requests_oauthlin.OauthSession(
client, redirect_uri=redirect_uri, scope=scope, state=state)
return oauth
# View callables
def home(request):
"""Display 'Login with Keycloak' link."""
auth_url = request.registry.settings["oauth2.url.auth"]
state = secrets.token_urlsafe()
request.sesion["oauth2_state"] = state
oauth = get_oauth2_session(request, state)
authorization_url, state2 = oauth.authorization_url(auth_url)
if state2 != state:
log.error("STATE MISMATCH: %r != %r", state2, state)
return {"authorization_url": authorization_url}
def login(request):
"""Receive redirect from Keycloak server, display userinfo dict."""
secret = request.registry.settings["oauth2.secret"]
token_url = request.registry.settings["oauth2.url.token"]
userinfo_url = request.registry.settings["oauth2.url.userinfo"]
state = request.session{"oauth2_state"]
oauth = get_ouath2_session(request, state)
token = oauth.fetch_token(
token_url, client_secret=secret, authorization_response=request.url)
# Token dict:
# access_token: string
# expires_in: 300
# refresh_expires_in: 600
# refresh_token: string
# token_type: "bearer"
# not-before-policy: 0
# session_state: string
# scope: ["profile", "email"]
# expires_at: float
data = oauth.get(userinfo_url).json()
# Userinfo dict:
# sub: string (a short token)
# email_verified: False
# name: string (full name)
# preferred_username: string (user ID)
# given_name: string (first name)
# family_name: string (last name)
# email: string (email address)
return {"formatted_data": pprint.pformat(data)}
======
Questions from top to bottom:
- Am I matching the state correctly? The 'authorization_url' method
returns the state but the 'fetch_token' and 'get' methods don't so I
don't have anything to compare it against.
- Should I create a new state every time it displays the login link?
- Would this be amenable to Pyramid's CSRF token checking? But the
login request is GET and Pyramid's checking seems to be only for POST
requests.
- Should I delete the state from the session after matching it?
- What should I do if there's a state mismatch? I don't want to give
the user an Internal Server Error, or tell them they're bad when
there's nothing they can do about it.
- What should I do with the token?
- Should I save the token in the session?
- What's the difference between the token keys 'expires_in' and
'refresh_expires_in'?
- What does the token's 'session_state' key mean? Should I care about it?
- What does the userinfo's 'sub' key mean? Should I care about it?
- Should I refresh the token? Where would I do that? In every view
callable? My existing LDAP implementation doesn't have this concept;
it leaves the user logged in until the Pyramid session expires and is
deleted in Redis.
- I'm instantiating the OAuth2Session in each view. Should I save it
somewhere? I can't put it in the Pyramid session because it's not
JSONable. Is it expensive to reinstantiate it like this?
On Tue, May 21, 2019 at 6:45 PM Jonathan Vanasco <[email protected]> wrote:
> oAuth Flows and Grants
>
> i suggest you take another look at the Authorization Code flow
>
> * https://nordicapis.com/8-types-of-oauth-flows-and-powers/
> * https://oauth.net/2/
> * https://auth0.com/docs/api-auth/which-oauth-flow-to-use
> *
> https://medium.com/@darutk/diagrams-and-movies-of-all-the-oauth-2-0-flows-194f3c3ade85
> * https://requests-oauthlib.readthedocs.io/en/latest/oauth2_workflow.html
>
> Let me explain a bit of the Authorization Code Grant using my library,
> because that sounds like what you should be doing (that flow, not my
> library)...
>
> 1. The user visits your Application, and is redirected to the Authority with
> a payload which contains the application id, a random id, a set of
> credentials to authorize, what they want, and callback URL that you already
> configured as valid with the authority. At the Authority, they will either
> login or be logged in, and Authorize the grant.
>
> This Pyramid view is a user being redirected from your application:
> https://github.com/jvanasco/pyramid_oauthlib_lowlevel/blob/master/pyramid_oauthlib_lowlevel/tests/oauth2_app/views.py#L654-L671
>
> 2. At the Authority, they generate a temporary oAuth Grant Token to track the
> authorization and redirect the user to a predetermined callback page on your
> application. There is some oAuth information in the headers of the redirect.
> This is the user at the Authority view-
> https://github.com/jvanasco/pyramid_oauthlib_lowlevel/blob/master/pyramid_oauthlib_lowlevel/tests/oauth2_app/views.py#L178-L241
> The GET params on the request to the authority will have a clientId
> (your app), a redirect url (which must match the one in their database) and a
> 'state' nonce/session
> The authority allows the Authenticated user to Authorize the credentials
> request request, create a session on their side, then redirect to your page.
>
> 3. Your application processes the oAuth information contained in the headers
> on the callback page, and then (4) redirects to a success page or the page
> originally requested. A library will handle processing all this stuff for
> you, you should just be pulling the information from validated request via
> the library's API.
>
> This is where that happens
> https://github.com/jvanasco/pyramid_oauthlib_lowlevel/blob/master/pyramid_oauthlib_lowlevel/tests/oauth2_app/views.py#L673-L724
>
> While this is going on, there are some behind the scenes communications
> between your server and the Authority.
>
> Within Step3, your application will make a request behind-the-scenes to the
> authority with the information from the callback. The authority will generate
> a "Server Bearer Token" on their side, and send you a payload with the
> "Client Bearer Token", which you save on your side and associate with the
> user in the session. At this point, the "Grant Token" should cease to
> exists, and both the client (your app) and the authority server will now have
> a stored "BearerToken".
>
> Generally speaking, once someone makes an oAuth authorization for your
> application to the Identity authority, the authority should pick up a
> subsequent request and just redirect to the callback and use the existing
> BearerToken -- instead of asking them to authorize again. Some authorities
> work differently though.
>
> The authorized BearerTokens are generally time-limited and provided with two
> components - an "Access Token" and a "Refresh Token". The Access Token is a
> secret string you make all your various requests with and has the expiry time
> limit; the Refresh Token is a different secret string you use to obtain a new
> token. If you control both servers, you may not want to time-limit them.
>
> After you complete the oAuth grant, you can use the stored token to make
> automated queries against the upstream server's API to update the user's
> profile information, ensure they are still registered, etc. The openid
> connect stuff basically bootstraps a bunch of profile information into the
> callback payloads. While that is useful, it's generally a one-time data
> transfer (unless there is some way to resync data), and having the oAuth
> token is preferred.
>
> I hope this quick overview makes sense.
--
You received this message because you are subscribed to the Google Groups
"pylons-discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To post to this group, send email to [email protected].
To view this discussion on the web visit
https://groups.google.com/d/msgid/pylons-discuss/CAH9f%3DuqkpRaTFiYQcGdrVTX-nQVRAnAgn-_i86JLpif%2BxQXWcQ%40mail.gmail.com.
For more options, visit https://groups.google.com/d/optout.