fastapi
fastapi copied to clipboard
[QUESTION] Keycloak integration
First check
-
[x] I used the GitHub search to find a similar issue and didn't find it.
-
[x] I searched the FastAPI documentation, with the integrated search.
-
[x] I already searched in Google "How to X in FastAPI" and didn't find any information.
Description
I have to implement a secure API using Keycloak as authentication provider. By reading the documentation i understood that for implementing third party authorization providers is mandatory to build a dependence to inject in the API I want to protect. Can you please explain me better what should i do from Fastapi to obtain a Keycloack token and check if it's valid or not? Cause OAuth2 it's a big topic and i'm a little confused
I managed to get part of this working, specifically checking if the token is valid with the help of the python-keycloak library.
import logging
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2AuthorizationCodeBearer
from fastapi.middleware.cors import CORSMiddleware
# Keycloak setup
from keycloak import KeycloakOpenID
keycloak_openid = KeycloakOpenID(
server_url="https://blah.blah/auth/",
client_id="blah",
realm_name="blah",
)
app = FastAPI()
oauth2_scheme = OAuth2AuthorizationCodeBearer(authorizationUrl="", tokenUrl="")
async def get_current_user(token: str = Depends(oauth2_scheme)):
try:
KEYCLOAK_PUBLIC_KEY = (
"-----BEGIN PUBLIC KEY-----\n"
+ keycloak_openid.public_key()
+ "\n-----END PUBLIC KEY-----"
)
return keycloak_openid.decode_token(
token,
key=KEYCLOAK_PUBLIC_KEY,
options={"verify_signature": True, "verify_aud": False, "exp": True},
)
except Exception as e:
logging.error(e)
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
@app.get("/user")
async def get_user(current_user: dict = Depends(get_current_user)):
logging.info(current_user)
return current_user
get_current_user
can be used to get the user for any request. The remaining question I have is what is the appropriate 0auth2 flow to use with Keycloak. I thought AuthorizationCodeBearer seemed appropriate, but I don't know what the correct authorizationUrl
and tokenUrl
I should provide so that users can authenticate requests from /docs.
@BernardZhao
I don't know what the correct
authorizationUrl
andtokenUrl
keycloak_url = "http://127.0.0.1:8080/auth/"
realm = "test"
oauth2_scheme = OAuth2AuthorizationCodeBearer(
authorizationUrl=f"{keycloak_url}realms/{realm}/protocol/openid-connect/auth",
tokenUrl=f"{keycloak_url}realms/{realm}/protocol/openid-connect/token",
)
Hi guys,
Supposing I want to do the following:
Which informations vue should send to fastapi ?
Do I need to use oauth2_scheme
or just keycloak_openid.decode_token
?
Using oauth2_scheme, it asks the following:
On frontend client I selected public access. I don't have client secret in this case. Fastapi is configured to use bearer-only access type.
Below is what I am currently using for FasAPI/Vue/KeyCloak. Firstly, I've got an auth file which contains the logic:
# ./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
from pydantic import Json
from .models import User
# This is just for fastapi docs
oauth2_scheme = OAuth2AuthorizationCodeBearer(
authorizationUrl=settings.auth.authorization_url, # https://sso.example.com/auth/
tokenUrl=settings.auth.token_url, # https://sso.example.com/auth/realms/example-realm/protocol/openid-connect/token
)
# This actually does the auth checks
keycloak_openid = KeycloakOpenID(
server_url=settings.auth.server_url, # https://sso.example.com/auth/
client_id=settings.auth.client_id, # backend-client-id
realm_name=settings.auth.realm, # example-realm
client_secret_key=settings.auth.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-----"
)
async def get_auth(token: str = Security(oauth2_scheme)) -> Json:
try:
return keycloak_openid.decode_token(
token,
key= await get_idp_public_key(),
options={
"verify_signature": True,
"verify_aud": True,
"exp": True
}
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=str(e), # "Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
async def get_current_user(
identity: Json = Depends(get_auth)
) -> User:
return User.first_or_fail(identity['sub']) # get your user form the DB using identity['sub']
Then you can optionally set a default client ID for the docs (which is useful for development)
# /main.py
from fastapi import FastAPI
from .config import settings
app = FastAPI(
title=settings.project_name,
debug=settings.app_debug,
openapi_url=f"{settings.api_v1_str}/openapi.json",
swagger_ui_init_oauth = {
# If you are using pkce (which you should be)
"usePkceWithAuthorizationCodeGrant": True,
# Auth fill client ID for the docs with the below value
"clientId": settings.auth.docs_client_id, # example-frontend-client-id-for-dev
"scopes": settings.auth.scopes # [required scopes here]
}
)
Now you can protect individual routes by adding Depends(get_auth)
. Or if you would like to protect a group of routes you can create an APIRouter object and add get_auth
as a dependency like this:
# global route collection
api_router = APIRouter(default_response_class=JSONResponse)
public_routes = APIRouter()
authenticated_routes = APIRouter()
authenticated_routes.include_router(
user_router, prefix='/user', tags=['User']
)
api_router.include_router(
public_routes
)
api_router.include_router(
authenticated_routes,
dependencies=[Depends(get_auth)]
)
app.include_router(api_router, prefix="/api/v1")
When using the docs to test your api, the client ID should now autofill the above value and you can leave the client secret blank (as we don't have one for public clients). One caveat though, is that I haven't worked out how to make the docs refresh the access token. There is a refresh_url
parameter on the OAuth2AuthorizationCodeBearer
, but this doesn't seem to work as I expected. If anyone could solve my token refresh issues, that would be most appreciated! :smiley:
Then on the Vue frontend side, I'm using:
import VueKeycloakJs from '@dsb-norge/vue-keycloak-js'
import router from './router'
import Vue from 'vue'
import App from './App'
Vue.config.productionTip = false
Vue.use(VueKeycloakJs, {
logout: {
redirectUri: `${process.env.VUE_APP_BASE_URL}` // Your home page
},
init: {
onLoad: 'login-required', // or 'check-sso' if you only want to protect some routes
pkceMethod: 'S256',
enableLogging: false // set to true for debugging
},
config: {
url: `${process.env.VUE_APP_OIDC_URL}`, // https://sso.example.com/auth/
clientId: `${process.env.VUE_APP_OIDC_CLIENT_ID}`, // frontend-client-id
realm: `${process.env.VUE_APP_OIDC_REALM}` // example-realm
},
onInitError: () => {
console.log('Error Loading Auth')
}
})
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
Hope that helps!
Hi @pet1330,
Thank you for your reply.
In this example, which access type are you using on each keycloak client (backend and frontend) ?
I have to create two client inside a realm in keycloak:
On backend side (fastapi) I defined the client as bearer-only - in that case keycloak will not attempt to authenticate users, but only verify bearer tokens.
On frontend side (vue), I defined the client as public - in that case user should login via GUI then only generate the token.
The updateToken function is handled using vue : vue - keycloak
In my case, because I defined public access type on frontend client, and bearer-only access type to backend, I don't have a client_secret
parameter (it is generated only for confidential access type). And it seems that OAuth2AuthorizationCodeBearer
requires client_secret
parameter.
As you mentioned, it seems oauth2_scheme
is just for fastapi docs, and maybe in my case it won't be necessary.
Thank you very much to share your approach.
The OAuth2AuthorizationCodeBearer
doesn't require a client secret. In my example above, I provide one only to the KeycloakOpenID
(which is also optional, and in your case would be left blank). The Vue frontend is a public
client. On the FastAPI backend I'm using confidential
. Although our backend does not handle the login process itself (this is done on the frontend as per your diagram), it does need to perform some API operations against KeyCloak to allow the resource server to modify some authorisation configuration. However, if you're not using KeyCloak to manage your authentication then bearer-only should work as well :crossed_fingers:, you just need to leave the client secret blank, both when initialising the OAuth2AuthorizationCodeBearer
and KeycloakOpenID
objects, and on the docs page.
# This is just for fastapi docs
oauth2_scheme = OAuth2AuthorizationCodeBearer(
authorizationUrl=settings.auth.authorization_url, # https://sso.example.com/auth/
tokenUrl=settings.auth.token_url, # https://sso.example.com/auth/realms/example-realm/protocol/openid-connect/token
)
# This actually does the auth checks
keycloak_openid = KeycloakOpenID(
server_url=settings.auth.server_url, # https://sso.example.com/auth/
client_id=settings.auth.client_id, # backend-client-id
realm_name=settings.auth.realm, # example-realm
+ # client_secret_key=settings.auth.client_secret,
- client_secret_key=settings.auth.client_secret,
verify=True
)
In our case, we use dsb-norge/vue-keycloak-js to automatically handle refreshing the access token, but your gist above should also work :smiley:
Hi @pet1330, thanks again.
When I set depends(oauth2_scheme )
, after filling client_id on /docs for bearer-only access type, it redirects to this message:
Using confidential access type on backend, it seems to pass:
I don't know if I am doing something wrong.
You can't use your FastAPI (your bearer only client ID) on the /docs
page. The client ID on /docs
page should be a public
client ID.
You should think of the /docs
as a frontend application, separate from your API server (even though FastAPI creates it for you). If you use your FastAPI (your bearer-only client ID) on the /docs
page, then it will not allow you to login as it is only to validate tokens and cannot be used to login (hence the message you're getting).
So you should add your bearer-only token to the KeycloakOpenID
object used to validate tokens. Then when you go to your /docs
page, enter your public
client ID
Hi @pet1330, it worked using a public (frontend) client ID.
I was having a problem related to origin has been blocked by CORS policy: No 'Access-Control-Allow-Origin'
, but it was also my fault. Web-origins was misconfigured on keycloak, but I could fix it.
I will make some more tests with vue, in order to understand a good way to send token information via API using axios.
Thank you very much for helping me.
Unfortunately Swagger UI still seems unable to automatically refresh the access tokens when they are expired: https://github.com/swagger-api/swagger-ui/issues/7257
Hi guys @kaleming @pet1330
I was integrating keycloak into my project and following the steps I have seen here it works, but when I want to test the authentication with swagger, I get the following error "Invalid parameter: redirect_uri". Did anything similar happen to you?
Thanks a bunch for your approaches!
@aguilarpablo, I think this might be an issue with your keycloak client setup. When you view the client in the keycloak admin, check what value you have for "Valid Redirect URIs", as this is a list of allowed redirect values. Ideally, this should be exactly the location you want the client to redirect to (and should match your client config), but off the top of my head I think you may also be able to put a *
as a wildcard in this box while developing to allow anywhere to be a valid redirect. Obviously, in production, be specific! 😄