Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keycloak integration #1428

Closed
3 tasks done
acutaia opened this issue May 18, 2020 · 12 comments
Closed
3 tasks done

Keycloak integration #1428

acutaia opened this issue May 18, 2020 · 12 comments
Labels
question Question or problem question-migrate

Comments

@acutaia
Copy link

acutaia commented May 18, 2020

First check

  • I used the GitHub search to find a similar issue and didn't find it.

  • I searched the FastAPI documentation, with the integrated search.

  • 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

@acutaia acutaia added the question Question or problem label May 18, 2020
@BernardZhao
Copy link

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.

@art1415926535
Copy link

@BernardZhao

I don't know what the correct authorizationUrl and tokenUrl

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",
)

@kaleming
Copy link

Hi guys,

Supposing I want to do the following:

image

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:

image

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.

@pet1330
Copy link

pet1330 commented Apr 14, 2021

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! 😃

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!

@kaleming
Copy link

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.

@pet1330
Copy link

pet1330 commented Apr 14, 2021

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 🤞, 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 😃

@kaleming
Copy link

kaleming commented Apr 14, 2021

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:

image

Using confidential access type on backend, it seems to pass:

image

I don't know if I am doing something wrong.

@pet1330
Copy link

pet1330 commented Apr 14, 2021

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

@kaleming
Copy link

kaleming commented Apr 14, 2021

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.

@major-mayer
Copy link

Unfortunately Swagger UI still seems unable to automatically refresh the access tokens when they are expired:
swagger-api/swagger-ui#7257

@pabloaguilarmartinez
Copy link

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!

@pet1330
Copy link

pet1330 commented Aug 4, 2022

@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! 😄

@tiangolo tiangolo changed the title [QUESTION] Keycloak integration Keycloak integration Feb 24, 2023
@fastapi fastapi locked and limited conversation to collaborators Feb 28, 2023
@tiangolo tiangolo converted this issue into discussion #9066 Feb 28, 2023

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
question Question or problem question-migrate
Projects
None yet
Development

No branches or pull requests

8 participants