Keycloak DevNotes
Integration: ApiLogicServer + Keycloak
This repo contains a demo for apilogicserver integration with keycloak oidc JWT authentication. We can now codegen kc-capable apps (though will still required setup as described below).
- kci refers to the running KeyCloakIntegration
Status - 3/10:
- able to access keycloak for login using modified sra at localhost.
- kci is running with authentication & authorization (minimal testing)
- we can now run a generated app, presuming the Iterim Additional Setup
Setup
ApiLogicServer add-auth --provider-type=keycloak --db-url=
Iterim Additional Setup
ApiLogicServer add-auth --provider-type=keycloak --db-url= # if not already done
cp -r devops/keycloak/nginx/admin-app ui/safrs-react-admin
Or, the entire sequence, using the testpy release (aside - the dev ide has run configs for this under 2. Create servers/ApiLogicProject):
ApiLogicServer create --project-name= --db-url=
code ApiLogicProject # then, in VSC terminal...
ApiLogicServer add-cust
ApiLogicServer add-auth --provider-type=keycloak --db-url=
cp -r devops/keycloak/nginx/admin-app ui/safrs-react-admin
cd devops/keycloak
docker compose up
# start ApiLogicServer server from the IDE (F5)
Run:
cd devops/keycloak
docker-compose up
This will run keycloak on the docker host (use admin, password): - keycloak (http://localhost:8080)
Define Users and Roles
This works - you can define users, and they are available (with their roles) at runtime (see the jwt.io screenshot, below):
Define Attributes
I can enter User Attributes:
And, using this, register mappers:
You must provide the Token Claim Name
:
Authorization working
Login as u1. Observe you only get 1 category.
Test:
1. Keycloak Login Unit Test
Verify keycloak login with demo, demo
# keycloak realm named "kcals"
KC_BASE=http://localhost:8080/realms/kcals
echo "\n\noidc token endpoint"
TOKEN_ENDPOINT=$(curl ${KC_BASE}/.well-known/openid-configuration | jq -r .token_endpoint)
echo "TOKEN_ENDPOINT: ${TOKEN_ENDPOINT} \n"
echo "retrieve an access token by logging in "
TOKEN=$(curl ${TOKEN_ENDPOINT} -d 'grant_type=password&client_id=alsclient' -d 'username=u1' -d 'password=p' | jq -r .access_token)
echo "TOKEN: ${TOKEN} \n"
# test the authentication
curl http://localhost:5656/api/Category -H "Authorization: Bearer ${TOKEN}" | jq .
2. Start APP Logic Server
Use first Run Config.
- If possible, I'd like to simplify setup, and make debugging easier, so trying to run the app natively.
Adapted Implementation
Several changes to adapt the original poc to API Logic Server structure:
- Updated
security/system/authentication.py
- Call a new
configure_auth
function in the Keycloak Provider - Pass jwt_data to
get_user(identity, jwt_data)
(arg 2, instead of password) - This removes dependency on this file to provider type.
- Call a new
- Introduced
security/authentication_provider/keycloak/auth_provider
- Moved the settings and
get_jwt_public_key
to there - This centralizes all the keycloak elements into its provider
- There is a
config/config.py
setting to activate the Keycloak Provider.- This will later be a CLI command.
- Moved the settings and
- Added the docker compose material (including imports) to the
devops
dir - Note interim SRA is included in
ui/safrs-react-admin
- To login, see the
Auth
object in the admin app: demo, demo
Inspecting Access Tokens
You can use jwt.io:
Initial Implementation (for reference)
- the
$PWD/projects
was mounted at/projects
in the ApiLogicServer container - A project named
KCALS
was created (default nw, with authentication):
mkdir projects
chmod 777 projects # we need to be able to write to this directory from the container
docker run $PWD/projects:/projects -it apilogicserver/api_logic_server bash -c "ApiLogicServer create --project_name=/projects/KCALS --db_url= ; ApiLogicServer add-auth --project_name=/projects/KCALS"
For users to be able to authenticate with JWTs signed by keycloak, we have to download the JWK signing key from keycloak and use that to validate the JWTs. JWT validation is implemented in https://github.com/thomaxxl/kcals/security/system/authentication.py.
By default, apilogicserver authentication uses a user database. Our users are defined in keycloak however. I had to change auth_provider.py for this to (kinda) work.
React-Admin
Nginx is used to host the safrs-react-admin frontend at http://localhost/admin-app .
Persisting Keycloak Data
keycloak data is stored inside the keycloak container in /opt/keycloak/data . To make this persistent, you should mount this volume. Currently, only the "import" folder is mounted. This import folder contains json files exported by me. These json files are imported when the container starts with the " --import-realm" command line switch ( https://www.keycloak.org/server/importExport )
You can try this:
$ mkdir data
$ mv import data # the import folder containing the json files
$ chmod 777 data # make sure the container keycloak user can write to this folder
Then, change the docker-compose keycloak volumes to:
volumes:
- $PWD/data:/opt/keycloak/data
Finally, update the docker-compose file so that the imports don't overwrite the mounted volume settings:
1. Remove the --import-realm
2. Remove - $PWD/import:/opt/keycloak/data/import
This way, the /opt/keycloak/data will remain on the docker host mounted directory ($PWD/data).
Access data - this does not appear to work (no cli):
docker cp keycloak:/opt/keycloak/data ~/Desktop/keycloak
Export the data
docker exec -it keycloak bash
$ cd /opt/keycloak
$ bin/kc.sh export --help
$ bin/kc.sh export --dir export
$ exit
docker cp keycloak:/opt/keycloak/export ~/Desktop/keycloak-export
Notes: Accessing the jwt at runtime
To retrieve user info from the jwt, you may want to look into these functions: https://flask-jwt-extended.readthedocs.io/en/stable/automatic_user_loading.html as used in security/system/authentication.py
Saved snippet
Aside - can use this as bearer... jwt.io will decode it
data = {
"grant_type": "password",
"client_id": "alsclient",
"username" :f"{username}",
"password": f"{password}"
}
resp = requests.post(f"{TOKEN_ENDPOINT}", data)
if resp.status_code == 200:
resp_data = json.loads(resp.text)
access_token = resp_data["access_token"]
return jsonify(access_token=access_token)