1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
use std::future::Future;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use crate::{prelude::reqwest, types::*, KeycloakError};
mod default_response;
mod generated_rest;
mod manual_rest;
mod url_enc;
pub use default_response::DefaultResponse;
pub struct KeycloakAdmin<TS: KeycloakTokenSupplier = KeycloakAdminToken> {
url: String,
client: reqwest::Client,
token_supplier: TS,
}
#[async_trait]
pub trait KeycloakTokenSupplier {
async fn get(&self, url: &str) -> Result<String, KeycloakError>;
}
#[derive(Clone)]
pub struct KeycloakServiceAccountAdminTokenRetriever {
client_id: String,
client_secret: String,
realm: String,
reqwest_client: reqwest::Client,
}
#[async_trait]
impl KeycloakTokenSupplier for KeycloakServiceAccountAdminTokenRetriever {
async fn get(&self, url: &str) -> Result<String, KeycloakError> {
let admin_token = self.acquire(url).await?;
Ok(admin_token.access_token)
}
}
impl KeycloakServiceAccountAdminTokenRetriever {
/// Creates a token retriever for a [service account] in the `master` realm.
///
/// Use this when you want to authenticate against Keycloak using a
/// confidential client whose `Service Accounts` feature is enabled and
/// whose `client_id` lives in the `master` realm.
///
/// To target a different realm, use [`KeycloakServiceAccountAdminTokenRetriever::create_with_custom_realm`].
///
/// [service account]: https://www.keycloak.org/docs/latest/server_development/#authenticating-with-a-service-account
///
/// # Arguments
///
/// * `client_id` - The client id of a client with the following characteristics:
/// 1. Exists in the `master` realm.
/// 2. `confidential` access type.
/// 3. `Service Accounts` option is enabled.
/// * `client_secret` - The secret credential assigned to the given `client_id`.
/// * `client` - A reqwest `Client` used to perform the token retrieval call.
///
/// # Example
///
/// ```no_run
/// # async fn doc() -> Result<(), keycloak::KeycloakError> {
/// use keycloak::{prelude::reqwest, KeycloakAdmin, KeycloakServiceAccountAdminTokenRetriever};
///
/// let client = reqwest::Client::new();
/// let url = "https://keycloak.example.com";
///
/// let retriever = KeycloakServiceAccountAdminTokenRetriever::create(
/// "my-client",
/// "my-secret",
/// client.clone(),
/// );
///
/// let admin = KeycloakAdmin::new(url, retriever, client);
/// // ... use `admin` to call the Admin REST API.
/// # let _ = admin;
/// # Ok(()) }
/// ```
pub fn create(client_id: &str, client_secret: &str, client: reqwest::Client) -> Self {
Self {
client_id: client_id.into(),
client_secret: client_secret.into(),
realm: "master".into(),
reqwest_client: client,
}
}
/// Creates a token retriever for a [service account] in a caller-specified realm.
///
/// This is the same as [`KeycloakServiceAccountAdminTokenRetriever::create`],
/// but the realm is supplied explicitly rather than defaulting to `master`.
///
/// [service account]: https://www.keycloak.org/docs/latest/server_development/#authenticating-with-a-service-account
///
/// # Arguments
///
/// * `client_id` - The client id of a client with the following characteristics:
/// 1. Exists in `realm`.
/// 2. `confidential` access type.
/// 3. `Service Accounts` option is enabled.
/// * `client_secret` - The secret credential assigned to the given `client_id`.
/// * `realm` - The Keycloak realm the `client_id` lives in.
/// * `client` - A reqwest `Client` used to perform the token retrieval call.
///
/// # Example
///
/// ```no_run
/// # async fn doc() -> Result<(), keycloak::KeycloakError> {
/// use keycloak::{prelude::reqwest, KeycloakAdmin, KeycloakServiceAccountAdminTokenRetriever};
///
/// let client = reqwest::Client::new();
/// let url = "https://keycloak.example.com";
///
/// let retriever = KeycloakServiceAccountAdminTokenRetriever::create_with_custom_realm(
/// "my-client",
/// "my-secret",
/// "my-realm",
/// client.clone(),
/// );
///
/// let admin = KeycloakAdmin::new(url, retriever, client);
/// // ... use `admin` to call the Admin REST API.
/// # let _ = admin;
/// # Ok(()) }
/// ```
pub fn create_with_custom_realm(
client_id: &str,
client_secret: &str,
realm: &str,
client: reqwest::Client,
) -> Self {
Self {
client_id: client_id.into(),
client_secret: client_secret.into(),
realm: realm.into(),
reqwest_client: client,
}
}
/// Fetches a fresh [`KeycloakAdminToken`] for the configured service account.
///
/// Each call performs a new HTTP request against Keycloak's token endpoint
/// using the `client_credentials` grant. To avoid the per-request round-trip,
/// cache the returned [`KeycloakAdminToken`] yourself (e.g. using its
/// [`KeycloakAdminToken::expires_in`] field) or implement a custom
/// [`KeycloakTokenSupplier`].
///
/// # Arguments
///
/// * `url` - Base URL of the Keycloak server (e.g. `https://keycloak.example.com`).
pub async fn acquire(&self, url: &str) -> Result<KeycloakAdminToken, KeycloakError> {
let realm = &self.realm;
let response = self
.reqwest_client
.post(format!(
"{url}/realms/{realm}/protocol/openid-connect/token",
))
.form(&[
("client_id", self.client_id.as_str()),
("client_secret", self.client_secret.as_str()),
("grant_type", "client_credentials"),
])
.send()
.await?;
Ok(error_check(response).await?.json().await?)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub struct KeycloakAdminToken {
access_token: String,
expires_in: usize,
#[serde(rename = "not-before-policy")]
not_before_policy: Option<usize>,
refresh_expires_in: Option<usize>,
refresh_token: Option<String>,
scope: String,
session_state: Option<String>,
token_type: String,
}
impl KeycloakAdminToken {
/// Returns the access token issued by Keycloak.
pub fn access_token(&self) -> &str {
&self.access_token
}
/// Returns the lifetime in seconds of the access token.
pub fn expires_in(&self) -> usize {
self.expires_in
}
/// Returns the `not-before-policy` value, if provided by Keycloak.
pub fn not_before_policy(&self) -> Option<usize> {
self.not_before_policy
}
/// Returns the lifetime in seconds of the refresh token, if a refresh
/// token was issued.
pub fn refresh_expires_in(&self) -> Option<usize> {
self.refresh_expires_in
}
/// Returns the refresh token, if one was issued.
pub fn refresh_token(&self) -> Option<&str> {
self.refresh_token.as_deref()
}
/// Returns the OAuth scope(s) associated with the token.
pub fn scope(&self) -> &str {
&self.scope
}
/// Returns the session state, if provided by Keycloak.
pub fn session_state(&self) -> Option<&str> {
self.session_state.as_deref()
}
/// Returns the token type (typically `Bearer`).
pub fn token_type(&self) -> &str {
&self.token_type
}
}
#[async_trait]
impl KeycloakTokenSupplier for KeycloakAdminToken {
async fn get(&self, _url: &str) -> Result<String, KeycloakError> {
Ok(self.access_token.clone())
}
}
impl KeycloakAdminToken {
pub async fn acquire(
url: &str,
username: &str,
password: &str,
client: &reqwest::Client,
) -> Result<KeycloakAdminToken, KeycloakError> {
Self::acquire_custom_realm(
url,
username,
password,
"master",
"admin-cli",
"password",
client,
)
.await
}
pub async fn acquire_custom_realm(
url: &str,
username: &str,
password: &str,
realm: &str,
client_id: &str,
grant_type: &str,
client: &reqwest::Client,
) -> Result<KeycloakAdminToken, KeycloakError> {
let response = client
.post(format!(
"{url}/realms/{realm}/protocol/openid-connect/token",
))
.form(&[
("username", username),
("password", password),
("client_id", client_id),
("grant_type", grant_type),
])
.send()
.await?;
Ok(error_check(response).await?.json().await?)
}
}
async fn error_check(response: reqwest::Response) -> Result<reqwest::Response, KeycloakError> {
if !response.status().is_success() {
let status = response.status().into();
let text = response.text().await?;
return Err(KeycloakError::HttpFailure {
status,
body: serde_json::from_str(&text).ok(),
text,
});
}
Ok(response)
}
impl<TS: KeycloakTokenSupplier> KeycloakAdmin<TS> {
pub fn new(url: &str, token_supplier: TS, client: reqwest::Client) -> Self {
Self {
url: url.into(),
client,
token_supplier,
}
}
pub fn realm<'a>(&'a self, realm: &'a str) -> KeycloakRealmAdmin<'a, TS> {
KeycloakRealmAdmin { realm, admin: self }
}
}
pub struct KeycloakRealmAdmin<'a, TS: KeycloakTokenSupplier> {
pub realm: &'a str,
pub(crate) admin: &'a KeycloakAdmin<TS>,
}
pub trait KeycloakRealmAdminMethod {
type Output;
type Args: Default;
fn opts(
self,
args: Self::Args,
) -> impl Future<Output = Result<Self::Output, KeycloakError>> + Send;
fn with_default<F>(
self,
f: F,
) -> impl Future<Output = Result<Self::Output, KeycloakError>> + Send
where
Self: Sized,
Self::Args: Default,
F: FnOnce(Self::Args) -> Self::Args,
{
self.opts(f(Default::default()))
}
#[cfg(feature = "builder")]
fn builder<'m>(self) -> crate::builder::Builder<'m, Self>
where
Self: 'm + Sized,
{
From::from(self)
}
}