bleumink / streamlit-keycloak

User authentication and single sign-on in your Streamlit app using Keycloak
MIT License
59 stars 11 forks source link

How to get a user's roles ? #12

Open abnerjacobsen opened 1 year ago

abnerjacobsen commented 1 year ago

First of all, congratulations on your project, it works very well.

Now a question: I was able to install streamlit-keycloack and authenticate a user in my app on Streamlit Cloud using the Keycloack server that I configured, including obtaining the list of groups to which the user belongs. There's only one thing I don't know how to do, which is to get the list of user roles. Do you have any tips on how I can do this?

CHerSun commented 1 year ago

If your client has mapped roles - roles are returned under user_info -> resources -> client name -> roles, or do you mean something else?

Using demo app from the README.md: image

c-bik commented 1 year ago

Hi @CHerSun

I'm with the same question as @abnerjacobsen. I followed your answer and configured client->user role mapping:

image


I don't see any user_info -> resources property though!

image


My app.py is:

from dataclasses import asdict
from streamlit_keycloak import login
import streamlit as st

def main():
    st.subheader(f"Welcome {keycloak.user_info['preferred_username']}!")
    st.write(f"Here is your user information:")
    st.write(asdict(keycloak)["user_info"])
    st.write(f"Here is your OAuth2 token: {keycloak.access_token}")

st.title("Streamlit Keycloak example app1")
keycloak = login(
    url="http://auth.localhost:8080",
    realm="master",
    client_id="app1",
)

if keycloak.authenticated:
    main()

Am I missing a config or something?

c-bik commented 1 year ago

Had to turn on "Add to userinfo" here...

image

To get the roles..

image

It's apparently default disabled the latest version of Keycloak.

Hi @CHerSun

I'm with the same question as @abnerjacobsen. I followed your answer and configured client->user role mapping:

image

I don't see any user_info -> resources property though!

image

My app.py is:

from dataclasses import asdict
from streamlit_keycloak import login
import streamlit as st

def main():
    st.subheader(f"Welcome {keycloak.user_info['preferred_username']}!")
    st.write(f"Here is your user information:")
    st.write(asdict(keycloak)["user_info"])
    st.write(f"Here is your OAuth2 token: {keycloak.access_token}")

st.title("Streamlit Keycloak example app1")
keycloak = login(
    url="http://auth.localhost:8080",
    realm="master",
    client_id="app1",
)

if keycloak.authenticated:
    main()

Am I missing a config or something?

MPerrymanAW commented 1 year ago

Hi, just passing and saw this, it's not closed and c-bik's solution is nice and simple (didn't know that was there so thanks) but it's outputting all roles under the individual Clients, my solution to this problem is to add a Mapper to the Client for a specific Client which can then be merged under the same Token (although the 'User Client Role' may be able to be modified for this) My solution also allows for specific Roles to be exposed through the mapper for other Clients if you wanted to only allow specific Role(s) to be available. My notes are based on using v21.0.0 (Docker Image keycloak/keycloak:21.0.0-0)

As far as I'm aware your Client should be added to a new Realm, the 'master' Realm (as per the screenshots) shouldn't be used, although I'm guessing this was just to get the screenshots.

  1. When you create a new Client (and get the URLs perfect so it all works) there is a 'Client scope' area within the 'Client' named after the Client; '-dedicated', it should be the only link available, open it
  2. Under the 'Mappers' section click the 'Configure a new mapper' (or 'Add mapper'->'By configuration' if one already exists)
  3. Pick 'User Client Role' from the list
  4. Now fill in: a. Name: Whatever you want to call this configuraion, I've gone with ''s Roles' b. Client ID: Select your Client c. Client Role prefix: Potentially leave blank, I've used the name of the client here with an underscore as you can configure another Client to the same 'Token Claim Name' (another setting below) and if both Clients have the same Role names then they are made unique by this prefix d. Multivalued: On - If left Off then the Roles are comma-separated e. Token Claim Name: I've gone with 'roles', using dots in the name splits the data into children (clicking the '?' next to the title will explain this better) f. Add to userinfo: On - From testing I found this was the only option required to be On for the Roles to be included in the userinfo image

Sticking with the default one ('Client Scopes'->'roles'->'Mappers'->'client roles'), it's not do-able as the '${client_id}' only works in the 'Token Claim Name': image image

yaffol commented 1 year ago

If you don't have (or can't) enable 'Add to userinfo', you can extract the roles from the JWT.

Here's how I did it

import base64
import json
def decode_token(encoded_token):
    payload_part = encoded_token.split('.')[1]
    # Adding padding if necessary
    payload_part += '=' * (-len(payload_part) % 4)

    # base64Url decode
    decoded_bytes = base64.urlsafe_b64decode(payload_part)

    # Parse as JSON
    payload = json.loads(decoded_bytes)
    return payload
def main():
    # st.subheader(f"Welcome {keycloak.user_info['preferred_username']}!")
    st.write(f"Here is your user information:")
    st.write(asdict(keycloak))
    access_token = keycloak.access_token
    payload = decode_token(access_token)

    # Now, you can access the roles like this:
    roles = payload['realm_access']['roles']
    st.write(roles)

Which returns something like

[ "ROLE_USER", "offline_access", "uma_authorization" ]

c-bik commented 1 year ago

Thanks @MPerrymanAW and @yaffol very nice 👍