Skip to main content

arminius_lib_authentication/
lib.rs

1// SPDX-FileCopyrightText: 2025 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Functionality for handling authentication of clients
6
7pub mod test_utils;
8mod tests;
9
10use std::collections::HashSet;
11
12use arminius_lib_component::{
13	ArminiusConfig, ServerState,
14	error::{InternalServerError, MatrixApiError, ResultExt as _},
15};
16use arminius_lib_db_authentication::AuthenticationQueries as _;
17use arminius_lib_reqwest::setup_insecure_reqwest_client;
18use axum::{
19	extract::FromRequestParts,
20	http::{StatusCode, request::Parts},
21};
22use jsonwebtoken::{DecodingKey, jwk};
23use ruma::{
24	OwnedDeviceId, OwnedUserId, UserId,
25	api::error::{ErrorKind, UnknownTokenErrorData},
26	exports::serde::{Deserialize, Serialize},
27};
28use serde::de::DeserializeOwned;
29use url::Url;
30
31/// Error message for failed OIDC well known request
32const OIDC_WELL_KNOWN_REQUEST_FAILED: &str = "OIDC well known request failed";
33/// Error message for failed OIDC well known response
34const OIDC_WELL_KNOWN_RESPONSE_ERROR: &str = "OIDC well known response error";
35/// Error message for failed OIDC JWK request
36const OIDC_JWKS_REQUEST_FAILED: &str = "OIDC jwks request failed";
37/// Error message for failed OIDC JWK response
38const OIDC_JWKS_RESPONSE_ERROR: &str = "OIDC jwks response error";
39/// Error message for failed JWK algorithm processing
40const CANNOT_PROCESS_JWK_ALGORITHM: &str = "Cannot process JWK algorithm";
41/// Error message for failed JWK processing
42const CANNOT_PROCESS_JWK: &str = "Cannot process JWK";
43
44/// The session information an authenticated token is mapped to.
45#[derive(Serialize, Deserialize, Debug, Clone)]
46pub struct SessionInfo {
47	/// The user ID.
48	#[serde(rename = "sub")]
49	pub user_id: OwnedUserId,
50
51	/// The device ID.
52	pub device_id: OwnedDeviceId,
53}
54
55/// The claims contained in an authenticated token.
56#[derive(Serialize, Deserialize, Debug, Clone)]
57struct Claims {
58	/// The issuance timestamp should increase monotonically.
59	/// Therefore, it is used as the token tag in `fetch_session_info`.
60	/// <https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.6>
61	#[serde(rename = "iat")]
62	issued_at: i64,
63
64	/// The subject, to be mapped to a user.
65	/// <https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.2>
66	#[serde(rename = "sub")]
67	subject: String,
68
69	/// The session ID, to be mapped to a device.
70	/// <https://openid.net/specs/openid-connect-frontchannel-1_0.html#rfc.section.3>
71	#[serde(rename = "sid")]
72	session_id: String,
73}
74
75/// Error type for authentication errors
76#[derive(MatrixApiError, thiserror::Error, Debug)]
77pub enum AuthError {
78	/// The auth header is missing
79	#[error("Token header is missing")]
80	#[matrix_api_error(status_code = StatusCode::UNAUTHORIZED, error_kind = ErrorKind::UnknownToken(UnknownTokenErrorData::new()))]
81	MissingHeader,
82
83	/// Wrong encoding
84	#[error("Token header is not valid UTF-8")]
85	#[matrix_api_error(status_code = StatusCode::UNAUTHORIZED, error_kind = ErrorKind::UnknownToken(UnknownTokenErrorData::new()))]
86	WrongEncoding,
87
88	/// The token is unknown
89	#[error("No bearer token found")]
90	#[matrix_api_error(status_code = StatusCode::UNAUTHORIZED, error_kind = ErrorKind::UnknownToken(UnknownTokenErrorData::new()))]
91	UnknownToken,
92
93	/// Decoding the token failed
94	#[error("Decoding token failed")]
95	#[matrix_api_error(status_code = StatusCode::UNAUTHORIZED, error_kind = ErrorKind::UnknownToken(UnknownTokenErrorData::new()))]
96	DecodeError,
97
98	/// The session is unknown
99	#[error("Session is unknown")]
100	#[matrix_api_error(status_code = StatusCode::UNAUTHORIZED, error_kind = ErrorKind::UnknownToken(UnknownTokenErrorData::new()))]
101	UnknownSession,
102
103	/// Internal server error
104	#[error(transparent)]
105	InternalServerError(#[from] InternalServerError),
106
107	/// JWK unauthorized
108	#[error(transparent)]
109	JWKError(#[from] JWKError),
110}
111
112/// Error type for authentication errors
113#[derive(MatrixApiError, thiserror::Error, Debug)]
114pub enum JWKError {
115	/// Internal server error
116	#[error(transparent)]
117	InternalServerError(#[from] InternalServerError),
118
119	/// JWK unauthorized
120	#[error("User is not authorized")]
121	#[matrix_api_error(status_code = StatusCode::UNAUTHORIZED, error_kind = ErrorKind::UnknownToken(UnknownTokenErrorData::new()))]
122	Unauthorized,
123}
124
125/// The extractor to get the `Claims` of a request. Implements FromRequestParts,
126/// see `client_component::account_data::global_account_data_set` for an example
127#[derive(Debug, Clone)]
128pub struct AuthenticatedClaims(pub SessionInfo);
129
130/// Converts a value through a JSON round-trip
131fn json_convert<I: Serialize, O: DeserializeOwned>(input: I) -> Result<O, serde_json::Error> {
132	serde_json::from_value(serde_json::to_value(input)?)
133}
134
135/// Retrieves the JWK set from the OIDC provider.
136async fn fetch_jwk_set(
137	config: &ArminiusConfig,
138	oidc_well_known: &Url,
139) -> Result<jwk::JwkSet, InternalServerError> {
140	#[derive(serde::Deserialize)]
141	struct AuthMetadata {
142		/// URL of the authorization server's JWK Set document.  The
143		/// referenced document contains the signing key(s) the client
144		/// uses to validate signatures from the authorization server.
145		jwks_uri: Url,
146	}
147
148	// We use the "insecure" client here, because we allow administrators to set
149	// up their OIDC provider behind http
150	let client = setup_insecure_reqwest_client(config)
151		.unrecoverable_error("Could not set up reqwest client. Invalid proxy config?")?;
152	let auth_metadata_req = client.get(oidc_well_known.clone());
153	let auth_metadata_resp =
154		auth_metadata_req.send().await.unrecoverable_error(OIDC_WELL_KNOWN_REQUEST_FAILED)?;
155	let auth_metadata: AuthMetadata =
156		auth_metadata_resp.json().await.unrecoverable_error(OIDC_WELL_KNOWN_RESPONSE_ERROR)?;
157	let jwks_req = client.get(auth_metadata.jwks_uri);
158	let jwks_resp = jwks_req.send().await.unrecoverable_error(OIDC_JWKS_REQUEST_FAILED)?;
159
160	jwks_resp.json().await.unrecoverable_error(OIDC_JWKS_RESPONSE_ERROR)
161}
162
163/// Retrieves the JWK from the OIDC provider, if configured. Alternatively, use
164/// an internal JWK set for testing.
165async fn get_jwk(config: &ArminiusConfig, key_id: Option<String>) -> Result<jwk::Jwk, JWKError> {
166	let keys = if let Some(oidc_well_known) = &config.oidc_well_known {
167		fetch_jwk_set(config, oidc_well_known).await?
168	} else {
169		jwk::JwkSet { keys: vec![] }
170	}
171	.keys
172	.into_iter();
173
174	#[cfg(any(test, feature = "test_utils"))]
175	let keys = keys.chain(Some(test_utils::test_jwk()));
176
177	let mut keys = keys;
178
179	keys.find(|x| x.common.key_id == key_id).ok_or(JWKError::Unauthorized)
180}
181
182impl FromRequestParts<ServerState> for AuthenticatedClaims {
183	type Rejection = AuthError;
184
185	/// Decodes the authorization header
186	async fn from_request_parts(parts: &mut Parts, state: &ServerState) -> Result<Self, AuthError> {
187		let authorization_header =
188			parts.headers.get("Authorization").ok_or(AuthError::MissingHeader)?;
189		let authorization_header =
190			authorization_header.to_str().map_err(|_| AuthError::WrongEncoding)?;
191		let token = authorization_header.strip_prefix("Bearer ").ok_or(AuthError::UnknownToken)?;
192		let jwt_header = jsonwebtoken::decode_header(token).map_err(|_| AuthError::UnknownToken)?;
193		let jwk = get_jwk(&state.config, jwt_header.kid).await?;
194
195		let mut validation = jsonwebtoken::Validation::new(
196			json_convert(jwk.common.key_algorithm)
197				.unrecoverable_error(CANNOT_PROCESS_JWK_ALGORITHM)?,
198		);
199		validation.required_spec_claims = HashSet::new();
200		validation.validate_aud = false;
201
202		let decoding_key = DecodingKey::from_jwk(&jwk).unrecoverable_error(CANNOT_PROCESS_JWK)?;
203
204		let jwt = jsonwebtoken::decode::<Claims>(token, &decoding_key, &validation)
205			.map_err(|_| AuthError::UnknownToken)?;
206		let Claims { issued_at: iat, subject: sub, session_id: sid } = jwt.claims;
207		let db_session_info = state
208			.database
209			.clone()
210			.fetch_session_info(&sub, &sid, iat)
211			.await
212			.unrecoverable_error("Could not fetch session info from database")?
213			.ok_or(AuthError::UnknownSession)?;
214
215		Ok(AuthenticatedClaims(SessionInfo {
216			user_id: db_session_info
217				.user_id
218				.parse()
219				.unrecoverable_error("Failed to parse user ID")?,
220			device_id: db_session_info.device_id.into(),
221		}))
222	}
223}
224
225/// Returns true if the user is an admin
226#[must_use]
227pub fn check_user_is_admin(_: &UserId) -> bool {
228	true
229}