Skip to content

Commit

Permalink
intial release
Browse files Browse the repository at this point in the history
  • Loading branch information
ilyesAj committed Sep 27, 2023
1 parent 85ba26a commit ee228df
Show file tree
Hide file tree
Showing 15 changed files with 2,559 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,4 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.vscode/
14 changes: 14 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
uvicorn ="*"
fastapi ="*"
python-keycloak ="*"
pydantic ="*"
[dev-packages]

[requires]
python_version = "3.9"
396 changes: 396 additions & 0 deletions Pipfile.lock

Large diffs are not rendered by default.

26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,26 @@
# keycloak-fastAPI-integration
This repository illustrates how we can integrate keycloak with fastAPI for authetification

This repository illustrates how we can integrate keycloak with fastAPI for authetification.
This repo can be used as a template/code base for your app .


## setup env

I used `pipenv` for my env setup.

1. install `pipenv`
2. install dependencies using `pipenv install`
3. run keycloak instance using:

```bash
docker run -p 8080:8080 -v ./keycloak/keycloak_data:/opt/keycloak/data/h2 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:22.0.3 start-dev
```

4. now you can run your fastAPI app using `pipenv run python main.py`

## Demo

1. access to fastAPI swagger using http://127.0.0.1:8081/docs
2. get token using authorize
3. make your query for `/secure`
4.
68 changes: 68 additions & 0 deletions auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#/auth.py
from fastapi.security import OAuth2AuthorizationCodeBearer
from keycloak import KeycloakOpenID # pip require python-keycloak
from config import settings
from fastapi import Security, HTTPException, status,Depends
from pydantic import Json
from models import User

# This is used for fastapi docs authentification
oauth2_scheme = OAuth2AuthorizationCodeBearer(
authorizationUrl=settings.authorization_url, # https://sso.example.com/auth/
tokenUrl=settings.token_url, # https://sso.example.com/auth/realms/example-realm/protocol/openid-connect/token
)

# This actually does the auth checks
# client_secret_key is not mandatory if the client is public on keycloak
keycloak_openid = KeycloakOpenID(
server_url=settings.server_url, # https://sso.example.com/auth/
client_id=settings.client_id, # backend-client-id
realm_name=settings.realm, # example-realm
client_secret_key=settings.client_secret, # your backend client secret
verify=True
)

async def get_idp_public_key():
return (
"-----BEGIN PUBLIC KEY-----\n"
f"{keycloak_openid.public_key()}"
"\n-----END PUBLIC KEY-----"
)

# Get the payload/token from keycloak
async def get_payload(token: str = Security(oauth2_scheme)) -> dict:
try:
return keycloak_openid.decode_token(
token,
key= await get_idp_public_key(),
options={
"verify_signature": True,
"verify_aud": False,
"exp": True
}
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=str(e), # "Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)

# Get user infos from the payload
async def get_user_info(payload: dict = Depends(get_payload)) -> User:
try:
return User(
id=payload.get("sub"),
username=payload.get("preferred_username"),
email=payload.get("email"),
first_name=payload.get("given_name"),
last_name=payload.get("family_name"),
realm_roles=payload.get("realm_access", {}).get("roles", []),
client_roles=payload.get("realm_access", {}).get("roles", [])
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e), # "Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
12 changes: 12 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#/config.py
from models import authConfiguration


settings = authConfiguration(
server_url="http://localhost:8080/",
realm="roc",
client_id="rns:roc:portal",
client_secret="",
authorization_url="http://localhost:8080/realms/roc/protocol/openid-connect/auth",
token_url="http://localhost:8080/realms/roc/protocol/openid-connect/token",
)
Binary file added images/fastapi.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions keycloak/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# keycloak setup

Run keycloak using either docker-compose or docker:

```bash
docker run -p 8080:8080 -v ./keycloak_data:/opt/keycloak/data/h2 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:22.0.3 start-dev
```

You can also use your own instance of keycloak and just import the test realm using `roc.json`
16 changes: 16 additions & 0 deletions keycloak/docker-compose-keycloak.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
version: "3.7"

volumes:
keycloak:

services:

keycloak:
image: quay.io/keycloak/keycloak:22.0.3
ports:
- 8080:8080
environment:
- KEYCLOAK_ADMIN=admin
- KEYCLOAK_ADMIN_PASSWORD=admin
volumes:
- ./keycloak_data:/opt/keycloak/data/
6 changes: 6 additions & 0 deletions keycloak/keycloak_data/keycloakdb.lock.db
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#FileLock
#Wed Sep 27 11:50:36 GMT 2023
server=172.17.0.2\:39219
hostName=7cdda6a8b4fd
method=file
id=18ad67a07d7593dd7b27224d024b24bc4f456b7b17a
Binary file added keycloak/keycloak_data/keycloakdb.mv.db
Binary file not shown.
114 changes: 114 additions & 0 deletions keycloak/keycloak_data/keycloakdb.trace.db
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
2023-09-21 14:23:20 jdbc[3]: exception
org.h2.jdbc.JdbcSQLSyntaxErrorException: Table "MIGRATION_MODEL" not found (this database is empty); SQL statement:
SELECT ID, VERSION FROM MIGRATION_MODEL ORDER BY UPDATE_TIME DESC [42104-220]
2023-09-21 14:23:21 jdbc[3]: exception
org.h2.jdbc.JdbcSQLSyntaxErrorException: Table "DATABASECHANGELOG" not found (this database is empty); SQL statement:
SELECT COUNT(*) FROM PUBLIC.DATABASECHANGELOG [42104-220]
2023-09-21 14:23:21 jdbc[4]: exception
org.h2.jdbc.JdbcSQLSyntaxErrorException: Table "DATABASECHANGELOGLOCK" not found (this database is empty); SQL statement:
SELECT COUNT(*) FROM PUBLIC.DATABASECHANGELOGLOCK [42104-220]
2023-09-21 14:23:21 jdbc[3]: exception
org.h2.jdbc.JdbcSQLSyntaxErrorException: Table "DATABASECHANGELOG" not found; SQL statement:
SELECT COUNT(*) FROM PUBLIC.DATABASECHANGELOG [42102-220]
2023-09-21 14:29:40 jdbc[3]: exception
org.h2.jdbc.JdbcSQLNonTransientConnectionException: Database is already closed (to disable automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL) [90121-220]
at org.h2.message.DbException.getJdbcSQLException(DbException.java:690)
at org.h2.message.DbException.getJdbcSQLException(DbException.java:489)
at org.h2.message.DbException.get(DbException.java:223)
at org.h2.message.DbException.get(DbException.java:199)
at org.h2.message.DbException.get(DbException.java:188)
at org.h2.jdbc.JdbcConnection.checkClosed(JdbcConnection.java:1375)
at org.h2.jdbcx.JdbcXAConnection$PooledJdbcConnection.checkClosed(JdbcXAConnection.java:473)
at org.h2.jdbc.JdbcConnection.rollback(JdbcConnection.java:463)
at org.h2.jdbcx.JdbcXAConnection$PooledJdbcConnection.close(JdbcXAConnection.java:453)
at org.h2.jdbcx.JdbcXAConnection.close(JdbcXAConnection.java:76)
at io.agroal.pool.ConnectionHandler.closeConnection(ConnectionHandler.java:185)
at io.agroal.pool.ConnectionPool$DestroyConnectionTask.run(ConnectionPool.java:787)
at io.agroal.pool.ConnectionPool.close(ConnectionPool.java:196)
at io.agroal.pool.DataSource.close(DataSource.java:79)
at io.quarkus.agroal.runtime.DataSources.stop(DataSources.java:454)
at io.quarkus.agroal.runtime.DataSources_Bean.doDestroy(Unknown Source)
at io.quarkus.agroal.runtime.DataSources_Bean.destroy(Unknown Source)
at io.quarkus.agroal.runtime.DataSources_Bean.destroy(Unknown Source)
at io.quarkus.arc.impl.AbstractInstanceHandle.destroyInternal(AbstractInstanceHandle.java:82)
at io.quarkus.arc.impl.ContextInstanceHandleImpl.destroy(ContextInstanceHandleImpl.java:21)
at io.quarkus.arc.impl.AbstractSharedContext.destroy(AbstractSharedContext.java:96)
at io.quarkus.arc.impl.ArcContainerImpl.shutdown(ArcContainerImpl.java:468)
at io.quarkus.arc.Arc.shutdown(Arc.java:66)
at io.quarkus.arc.runtime.ArcRecorder$1.run(ArcRecorder.java:53)
at io.quarkus.runtime.StartupContext.runAllInReverseOrder(StartupContext.java:84)
at io.quarkus.runtime.StartupContext.close(StartupContext.java:73)
at io.quarkus.runner.ApplicationImpl.doStop(Unknown Source)
at io.quarkus.runtime.Application.stop(Application.java:208)
at io.quarkus.runtime.Application.stop(Application.java:155)
at io.quarkus.runtime.ApplicationLifecycleManager.run(ApplicationLifecycleManager.java:227)
at io.quarkus.runtime.Quarkus.run(Quarkus.java:71)
at org.keycloak.quarkus.runtime.KeycloakMain.start(KeycloakMain.java:98)
at org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand.run(AbstractStartCommand.java:37)
at picocli.CommandLine.executeUserObject(CommandLine.java:2026)
at picocli.CommandLine.access$1500(CommandLine.java:148)
at picocli.CommandLine$RunLast.executeUserObjectOfLastSubcommandWithSameParent(CommandLine.java:2461)
at picocli.CommandLine$RunLast.handle(CommandLine.java:2453)
at picocli.CommandLine$RunLast.handle(CommandLine.java:2415)
at picocli.CommandLine$AbstractParseResultHandler.execute(CommandLine.java:2273)
at picocli.CommandLine$RunLast.execute(CommandLine.java:2417)
at picocli.CommandLine.execute(CommandLine.java:2170)
at org.keycloak.quarkus.runtime.cli.Picocli.parseAndRun(Picocli.java:100)
at org.keycloak.quarkus.runtime.KeycloakMain.main(KeycloakMain.java:88)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at io.quarkus.bootstrap.runner.QuarkusEntryPoint.doRun(QuarkusEntryPoint.java:61)
at io.quarkus.bootstrap.runner.QuarkusEntryPoint.main(QuarkusEntryPoint.java:32)
2023-09-21 14:29:40 jdbc[4]: exception
org.h2.jdbc.JdbcSQLNonTransientConnectionException: Database is already closed (to disable automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL) [90121-220]
at org.h2.message.DbException.getJdbcSQLException(DbException.java:690)
at org.h2.message.DbException.getJdbcSQLException(DbException.java:489)
at org.h2.message.DbException.get(DbException.java:223)
at org.h2.message.DbException.get(DbException.java:199)
at org.h2.message.DbException.get(DbException.java:188)
at org.h2.jdbc.JdbcConnection.checkClosed(JdbcConnection.java:1375)
at org.h2.jdbcx.JdbcXAConnection$PooledJdbcConnection.checkClosed(JdbcXAConnection.java:473)
at org.h2.jdbc.JdbcConnection.rollback(JdbcConnection.java:463)
at org.h2.jdbcx.JdbcXAConnection$PooledJdbcConnection.close(JdbcXAConnection.java:453)
at org.h2.jdbcx.JdbcXAConnection.close(JdbcXAConnection.java:76)
at io.agroal.pool.ConnectionHandler.closeConnection(ConnectionHandler.java:185)
at io.agroal.pool.ConnectionPool$DestroyConnectionTask.run(ConnectionPool.java:787)
at io.agroal.pool.ConnectionPool.close(ConnectionPool.java:196)
at io.agroal.pool.DataSource.close(DataSource.java:79)
at io.quarkus.agroal.runtime.DataSources.stop(DataSources.java:454)
at io.quarkus.agroal.runtime.DataSources_Bean.doDestroy(Unknown Source)
at io.quarkus.agroal.runtime.DataSources_Bean.destroy(Unknown Source)
at io.quarkus.agroal.runtime.DataSources_Bean.destroy(Unknown Source)
at io.quarkus.arc.impl.AbstractInstanceHandle.destroyInternal(AbstractInstanceHandle.java:82)
at io.quarkus.arc.impl.ContextInstanceHandleImpl.destroy(ContextInstanceHandleImpl.java:21)
at io.quarkus.arc.impl.AbstractSharedContext.destroy(AbstractSharedContext.java:96)
at io.quarkus.arc.impl.ArcContainerImpl.shutdown(ArcContainerImpl.java:468)
at io.quarkus.arc.Arc.shutdown(Arc.java:66)
at io.quarkus.arc.runtime.ArcRecorder$1.run(ArcRecorder.java:53)
at io.quarkus.runtime.StartupContext.runAllInReverseOrder(StartupContext.java:84)
at io.quarkus.runtime.StartupContext.close(StartupContext.java:73)
at io.quarkus.runner.ApplicationImpl.doStop(Unknown Source)
at io.quarkus.runtime.Application.stop(Application.java:208)
at io.quarkus.runtime.Application.stop(Application.java:155)
at io.quarkus.runtime.ApplicationLifecycleManager.run(ApplicationLifecycleManager.java:227)
at io.quarkus.runtime.Quarkus.run(Quarkus.java:71)
at org.keycloak.quarkus.runtime.KeycloakMain.start(KeycloakMain.java:98)
at org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand.run(AbstractStartCommand.java:37)
at picocli.CommandLine.executeUserObject(CommandLine.java:2026)
at picocli.CommandLine.access$1500(CommandLine.java:148)
at picocli.CommandLine$RunLast.executeUserObjectOfLastSubcommandWithSameParent(CommandLine.java:2461)
at picocli.CommandLine$RunLast.handle(CommandLine.java:2453)
at picocli.CommandLine$RunLast.handle(CommandLine.java:2415)
at picocli.CommandLine$AbstractParseResultHandler.execute(CommandLine.java:2273)
at picocli.CommandLine$RunLast.execute(CommandLine.java:2417)
at picocli.CommandLine.execute(CommandLine.java:2170)
at org.keycloak.quarkus.runtime.cli.Picocli.parseAndRun(Picocli.java:100)
at org.keycloak.quarkus.runtime.KeycloakMain.main(KeycloakMain.java:88)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at io.quarkus.bootstrap.runner.QuarkusEntryPoint.doRun(QuarkusEntryPoint.java:61)
at io.quarkus.bootstrap.runner.QuarkusEntryPoint.main(QuarkusEntryPoint.java:32)
Loading

0 comments on commit ee228df

Please sign in to comment.