Expand description

actix-web-middleware-keycloak-auth

A middleware for Actix Web that handles authentication with a JWT emitted by Keycloak.

Setup middleware

Setting up the middleware is done in 2 steps:

  1. creating a KeycloakAuth struct with the wanted configuration
  2. passing this struct to an Actix Web service wrap() method
use actix_web::{App, web, HttpResponse};
use actix_web_middleware_keycloak_auth::{KeycloakAuth, DecodingKey, AlwaysReturnPolicy};

// const KEYCLOAK_PK: &str = "..."; // You should get this from configuration

// Initialize middleware configuration
let keycloak_auth = KeycloakAuth::default_with_pk(DecodingKey::from_rsa_pem(KEYCLOAK_PK.as_bytes()).unwrap());

App::new()
    .service(
        web::scope("/private")
            .wrap(keycloak_auth) // Every route in the service will leverage the middleware
            .route("", web::get().to(|| async { HttpResponse::Ok().body("Private") })),
    )
    .service(web::resource("/").to(|| async { HttpResponse::Ok().body("Hello World") }));

HTTP requests to GET /private will need to have a Authorization header containing Bearer [JWT] where [JWT] is a valid JWT that was signed by the private key associated with the public key provided when the middleware was initialized.

Require roles

You can require one or several specific roles to be included in JWT. If they are not provided, the middleware will return a 403 error.

let keycloak_auth = KeycloakAuth {
    detailed_responses: true,
    passthrough_policy: AlwaysReturnPolicy,
    keycloak_oid_public_key: DecodingKey::from_rsa_pem(KEYCLOAK_PK.as_bytes()).unwrap(),
    required_roles: vec![
        Role::Realm { role: "admin".to_owned() }, // The "admin" realm role must be provided in the JWT
        Role::Client {
            client: "backoffice".to_owned(),
            role: "readonly".to_owned()
        }, // The "readonly" role of the "backoffice" client must be provided in the JWT
    ],
};

There is also a KeycloakRoles extractor that can be used to get the list of roles extracted from the JWT. This can be useful if a handler must have a different behavior depending of whether a role is present or not (i.e. a role is not strictly necessary but you want to check if it is there anyway, without having to reparse the JWT). Doing this will give your handler a Vec of Role.

use actix_web::{HttpResponse, Responder};
use actix_web_middleware_keycloak_auth::{KeycloakRoles, Role};

async fn private(roles: KeycloakRoles) -> impl Responder {
    let roles: &Vec<Role> = &roles;
    HttpResponse::Ok().body(format!("{:?}", roles))
}

Use several authentication profiles

It is possible to setup multiple authentication profiles if, for example, multiple groups of routes require different roles.

use actix_web::{App, web, HttpResponse};
use actix_web_middleware_keycloak_auth::{KeycloakAuth, DecodingKey, Role, AlwaysReturnPolicy};

// const KEYCLOAK_PK: &str = "..."; // You should get this from configuration

// No role required
let keycloak_auth = KeycloakAuth::default_with_pk(DecodingKey::from_rsa_pem(KEYCLOAK_PK.as_bytes()).unwrap());

// Admin realm role is required
let keycloak_auth_admin = KeycloakAuth {
    detailed_responses: true,
    passthrough_policy: AlwaysReturnPolicy,
    keycloak_oid_public_key: DecodingKey::from_rsa_pem(KEYCLOAK_PK.as_bytes()).unwrap(),
    required_roles: vec![Role::Realm { role: "admin".to_owned() }],
};

App::new()
    .service(
        web::scope("/private")
            .wrap(keycloak_auth) // User must be authenticated
            .route("", web::get().to(|| async { HttpResponse::Ok().body("Private") })),
    )
    .service(
        web::scope("/admin")
            .wrap(keycloak_auth_admin) // User must have the "admin" role
            .route("", web::get().to(|| async { HttpResponse::Ok().body("Admin") })),
    )
    .service(web::resource("/").to(|| async { HttpResponse::Ok().body("Hello World") }));

Access claims from handlers

When authentication is successful, the middleware will store the decoded JWT claims so that they can be accessed from handlers.

We provide the KeycloakClaims as an Actix Web extractor, which means you can use it as a handler’s parameter to obtain claims. This extractor requires a type parameter: it is the struct you want claims to be deserialized into. This struct must implement Serde’s Deserialize trait.

use actix_web::{HttpResponse, Responder};
use actix_web_middleware_keycloak_auth::KeycloakClaims;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
pub struct MyClaims {
  any_fields: u32,
  that_money: String,
  can_buy: Vec<String>,
}

async fn private(claims: KeycloakClaims<MyClaims>) -> impl Responder {
    HttpResponse::Ok().body(format!("{:?}", &claims))
}

Standard claims

We provide the StandardKeycloakClaims type as a convenience extractor for standard JWT claims. It is equivalent as using the KeycloakClaims<StandardClaims> extractor. Check StandardClaims to see which claims are extracted.

use actix_web::{HttpResponse, Responder};
use actix_web_middleware_keycloak_auth::StandardKeycloakClaims;

async fn private(claims: StandardKeycloakClaims) -> impl Responder {
    HttpResponse::Ok().body(format!("{:?}", &claims))
}

All claims

It is possible, using the UnstructuredKeycloakClaims extractor, to get all provided claim in a semi-structured HashMap. This can be useful when you want to dynamically explore claims (i.e. claims’ structure is not fixed).

use actix_web::{HttpResponse, Responder};
use actix_web_middleware_keycloak_auth::UnstructuredKeycloakClaims;
use std::collections::HashMap;

async fn private(unstructured_claims: UnstructuredKeycloakClaims) -> impl Responder {
    let claims: &HashMap<String, serde_json::Value> = &unstructured_claims;
    HttpResponse::Ok().body(format!("{:?}", claims))
}

As a convenience method, it is also possible to extract and parse at once a given claim. The target type must implement Deserialize. If something fails, the returned Result will contain a ClaimError enum that can tell which one of the extraction or parsing step failed (and why).

use actix_web::{HttpResponse, Responder};
use actix_web_middleware_keycloak_auth::UnstructuredKeycloakClaims;
use std::collections::HashMap;

async fn private(unstructured_claims: UnstructuredKeycloakClaims) -> impl Responder {
    let some_claim = unstructured_claims.get::<Vec<String>>("some_claim");
    HttpResponse::Ok().body(format!("{:?}", &some_claim))
}

Make authentication optional

By default, when the middleware cannot authenticate a request, it immediately responds with a HTTP error (401 or 403 depending on what failed). This behavior can be overridden by defining a passthrough policy when creating the middleware.

We provide two policies:

It is also quite easy to build a custom policy by implementing the PassthroughPolicy trait, which allows to choose different actions (pass or return) depending on the authentication error (see AuthError). You can even use a closure directly:

use actix_web_middleware_keycloak_auth::{KeycloakAuth, DecodingKey, AuthError, PassthroughAction};

let keycloak_auth_admin = KeycloakAuth {
    detailed_responses: true,
    passthrough_policy: |e: &AuthError| {
        match e {
            AuthError::NoAuthorizationHeader => PassthroughAction::Pass,
            _ => PassthroughAction::Return,
        }
    },
    keycloak_oid_public_key: DecodingKey::from_rsa_pem(KEYCLOAK_PK.as_bytes()).unwrap(),
    required_roles: vec![],
};

When the middleware does not respond immediately (authentication succeeded or the passthrough policy says “pass”), it will always store the authentication status in request-local data. This KeycloakAuthStatus can be picked up from a following middleware or handler so you can do whatever you want.

use actix_web::web::ReqData;
use actix_web_middleware_keycloak_auth::KeycloakAuthStatus;

async fn my_handler(auth_status: ReqData<KeycloakAuthStatus>) -> impl Responder {
    match auth_status.into_inner() {
        KeycloakAuthStatus::Success => HttpResponse::Ok().body("success!"),
        KeycloakAuthStatus::Failure(e) => HttpResponse::Ok().body(format!("auth failed ({:?}) but it's OK", &e))
    }
}

Structs

  • Access details
  • A passthrough policy that will always continue to handler (i.e. when authentication is optional)
  • A passthrough policy that will always return an HTTP error (i.e. when authentication is mandatory)
  • (Re-exported from the jsonwebtoken crate) All the different kind of keys we can use to decode a JWT. This key can be re-used so make sure you only initialize it once if you can for better performance.
  • Middleware configuration
  • Internal middleware configuration
  • Actix Web extractor for custom JWT claims
  • Actix Web extractor for Keycloak roles
  • Standard JWT claims
  • All claims that were extracted from the JWT in an unstructured way (available as a HashMap)

Enums

Traits

  • Generic structure of a policy that defines what the middleware should do when authentication fails

Functions

Type Aliases