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.

Reply via email to