keycloak/rest/
mod.rs

1use async_trait::async_trait;
2use serde::{Deserialize, Serialize};
3
4use crate::{types::*, KeycloakError};
5
6mod generated_rest;
7mod url_enc;
8
9pub struct KeycloakAdmin<TS: KeycloakTokenSupplier = KeycloakAdminToken> {
10    url: String,
11    client: reqwest::Client,
12    token_supplier: TS,
13}
14
15#[async_trait]
16pub trait KeycloakTokenSupplier {
17    async fn get(&self, url: &str) -> Result<String, KeycloakError>;
18}
19
20#[derive(Clone)]
21pub struct KeycloakServiceAccountAdminTokenRetriever {
22    client_id: String,
23    client_secret: String,
24    realm: String,
25    reqwest_client: reqwest::Client,
26}
27
28#[async_trait]
29impl KeycloakTokenSupplier for KeycloakServiceAccountAdminTokenRetriever {
30    async fn get(&self, url: &str) -> Result<String, KeycloakError> {
31        let admin_token = self.acquire(url).await?;
32        Ok(admin_token.access_token)
33    }
34}
35
36impl KeycloakServiceAccountAdminTokenRetriever {
37    /// Creates a token retriever for a [service account](https://www.keycloak.org/docs/latest/server_development/#authenticating-with-a-service-account)
38    /// * `client_id` - The client id of a client with the following characteristics:
39    ///                  1. Exists in the **master** realm
40    ///                  2. `confidential` access type
41    ///                  3. `Service Accounts` option is enabled
42    /// * `client_secret` - The secret credential assigned to the given `client_id`
43    /// * `client` - A reqwest Client to perform the token retrieval call
44    pub fn create(client_id: &str, client_secret: &str, client: reqwest::Client) -> Self {
45        Self {
46            client_id: client_id.into(),
47            client_secret: client_secret.into(),
48            realm: "master".into(),
49            reqwest_client: client,
50        }
51    }
52
53    pub fn create_with_custom_realm(
54        client_id: &str,
55        client_secret: &str,
56        realm: &str,
57        client: reqwest::Client,
58    ) -> Self {
59        Self {
60            client_id: client_id.into(),
61            client_secret: client_secret.into(),
62            realm: realm.into(),
63            reqwest_client: client,
64        }
65    }
66
67    pub async fn acquire(&self, url: &str) -> Result<KeycloakAdminToken, KeycloakError> {
68        let realm = &self.realm;
69        let response = self
70            .reqwest_client
71            .post(format!(
72                "{url}/realms/{realm}/protocol/openid-connect/token",
73            ))
74            .form(&[
75                ("client_id", self.client_id.as_str()),
76                ("client_secret", self.client_secret.as_str()),
77                ("grant_type", "client_credentials"),
78            ])
79            .send()
80            .await?;
81        Ok(error_check(response).await?.json().await?)
82    }
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
86pub struct KeycloakAdminToken {
87    access_token: String,
88    expires_in: usize,
89    #[serde(rename = "not-before-policy")]
90    not_before_policy: Option<usize>,
91    refresh_expires_in: Option<usize>,
92    refresh_token: Option<String>,
93    scope: String,
94    session_state: Option<String>,
95    token_type: String,
96}
97
98#[async_trait]
99impl KeycloakTokenSupplier for KeycloakAdminToken {
100    async fn get(&self, _url: &str) -> Result<String, KeycloakError> {
101        Ok(self.access_token.clone())
102    }
103}
104
105impl KeycloakAdminToken {
106    pub async fn acquire(
107        url: &str,
108        username: &str,
109        password: &str,
110        client: &reqwest::Client,
111    ) -> Result<KeycloakAdminToken, KeycloakError> {
112        Self::acquire_custom_realm(
113            url,
114            username,
115            password,
116            "master",
117            "admin-cli",
118            "password",
119            client,
120        )
121        .await
122    }
123
124    pub async fn acquire_custom_realm(
125        url: &str,
126        username: &str,
127        password: &str,
128        realm: &str,
129        client_id: &str,
130        grant_type: &str,
131        client: &reqwest::Client,
132    ) -> Result<KeycloakAdminToken, KeycloakError> {
133        let response = client
134            .post(format!(
135                "{url}/realms/{realm}/protocol/openid-connect/token",
136            ))
137            .form(&[
138                ("username", username),
139                ("password", password),
140                ("client_id", client_id),
141                ("grant_type", grant_type),
142            ])
143            .send()
144            .await?;
145        Ok(error_check(response).await?.json().await?)
146    }
147}
148
149async fn error_check(response: reqwest::Response) -> Result<reqwest::Response, KeycloakError> {
150    if !response.status().is_success() {
151        let status = response.status().into();
152        let text = response.text().await?;
153        return Err(KeycloakError::HttpFailure {
154            status,
155            body: serde_json::from_str(&text).ok(),
156            text,
157        });
158    }
159
160    Ok(response)
161}
162
163fn to_id(response: reqwest::Response) -> Option<TypeString> {
164    response
165        .headers()
166        .get(reqwest::header::LOCATION)
167        .and_then(|v| v.to_str().ok())
168        .and_then(|v| v.split('/').last())
169        .map(From::from)
170}
171
172impl<TS: KeycloakTokenSupplier> KeycloakAdmin<TS> {
173    pub fn new(url: &str, token_supplier: TS, client: reqwest::Client) -> Self {
174        Self {
175            url: url.into(),
176            client,
177            token_supplier,
178        }
179    }
180}