So I can extract the data from my OAuth2 tokens now. My remaining
questions are how to integrate refreshing into a Pyramid application.

First, do I need to? I don't care if the token is refreshed; I'll keep
using the claims stored in the Pyramid session until it expires.But if
I want to contribute to the enterprise's Single Sign-In, do I need to
tell the server the user is still logged into my application so it
doesn't expire the SSO account? Do I do this by refreshing the token?

If I do want to refresh the token, do I do it in a NewRequest subscriber?
The refresh_expire is 60 minutes, so how close to the end should I do
the refresh? If a request comes in 10 minutes before the end, I don't
know whether the next request will be in 1 second or 20 minutes. What
if the request contains POST data? Would I have to save the data in
the session, generate an authorization URL, redirect to the server,
come back through the callback view, redirect back to the original
URL, and extract the POST data in the session. (Where it's no longer
in request.POST.) That sounds like a lot of code overhead.

I may also have to put a session lock in the site; i.e., Javascript
that waits for an idle timeout and puts up a modal dialog, "Do you
want to extend your session?" or "Your session is expired. Click here
to log in again." How would that interact with tokens and refreshing
tokens, since my token processing is in the backend? How could the
Javascript extend the session or have the user log in again without
throwing away a partially-filled-in form? I've gotten user complaints
about that, that the user leaves the page on a form and then come back
later and submits the form and blammo! the server-side session is
deleted so they have to log in again and reenter the form input from
scratch. So I want to avoid that.

On Sat, Jun 8, 2019 at 9:40 AM Mike Orr <sluggos...@gmail.com> wrote:
>
> To follow up, I got OAuth/Keycloak authentication working with the
> following code pattern. The userinfo contains only name/email-related
> attributes, not the role attributes and origin directory info I need
> to calculate Pyramid pincipals. (Origin directory = where the user is
> defined; e.g., an enterprise LDAP directory.)
>
> Our Keycloak admin thinks these are not in the userinfo but in the
> access token itself, which is not a random string as I thought but
> JWT-encoded JSON. I was able to Base64-decode the token and get what
> looks like JSON with a "JWT" key. I'm evaluating 'jwt' and a few other
> libraries and seeing if there's a public key I need to decrypt it. The
> Base64-decoded file is not fully JSON: the JWT value is binary, and
> the file ends without a closing quote and brace. Maybe that's a JWT
> format that predates JSON.
>
> My original intention was to calculate the group principals based on
> the Keycloak roles and origin directory info, and get rid of my User
> record in the database that contains these, or rather convert the User
> record to an archive of the user's latest login date and Keycloak
> attributes.
>
> But without the Keycloak roles I can't do that, so I'm falling back to
> the existing User records for that information. This means I can
> authorize users who already have a User record, but if a new Keycloak
> user comes in (somebody who's configured in Keycloak to access the
> application but doesn't have a User record), I'll either have to not
> support them or create a User record with default roles because I
> don't know what their roles should be. The project team is deciding
> whether to do this and what the default roles should be. The admins
> can modify the User records online, but somebody will have to do it
> before the user first logs in or soon afterward, because otherwise
> they'll have fewer permissions than they should.
>
> Here's the code again from my prototype app:
>
> ===
> 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)
>     requests.session["oauth2_state"] = state
>      return {"authorization_url": authorization_url}
>
>  def login(request):
>      """Callback page; receive authn from Keycloak server."""
>      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)}
>
>     # TODO: Should delete state in session?
>     # TODO: Should convert state to Pyramid CSRF token?
>
>     ### Configuration settings:
>     # oauth2.client: Client ID registered in Keycloak. String, required.
>     # oauth2.secret: Client secret registered in Keycloak. String, required.
>     # oauth2,scope: Oauth2 scopes. Space-delimited string, optional,
> default None.
>     # oauth2.url;auth: Server's authorization URL. String, required.
>     # oauth2.url.token: Server's fetch token URL.  String, required.
>     # oauth2.url.userinfo: Server's userinfo URL.  String, required.
> ======
>
> In my real application, the "home" page is protected, and the
> Unauthorized View displays the "login" page which contains a button to
> log in. The callback URL is "/oauth2/login", route "login_oauth2",
> view name "login_oauth2". It currently displays the userinfo and has
> links to continue to the originally-requested page or the home page.
> Ultimately it will redirect to these automatically.
>
> The callback page can only be used once after logging in. If the user
> refreshes the page I get an InvalidGrantError saying 'code' is wrong.
> I'm catching the error and displaying a page "Error from authorization
> server. Please log in again, [login link]".
>
> > - What does the userinfo's 'sub' key mean? Should I care about it?
>
> It's the "subject" of the userinfo, a UUID-like string. This is
> supposed to match a "sub" somewhere else in the protocol.



-- 
Mike Orr <sluggos...@gmail.com>

-- 
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 pylons-discuss+unsubscr...@googlegroups.com.
To post to this group, send email to pylons-discuss@googlegroups.com.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/pylons-discuss/CAH9f%3DuqA6vK%3D%3DnoqP1skXkwyHNOgxdESu6rUXdKF01TL5z9puQ%40mail.gmail.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to