1use std::net::SocketAddr;
9
10use axum::extract::{ConnectInfo, State};
11use axum::http::{HeaderValue, StatusCode, header};
12use axum::response::IntoResponse;
13use axum::{Json, response::Response};
14use tracing::{info, warn};
15
16use crate::error::{AuthError, AuthErrorKind};
17use crate::redirect_uri::is_allowed_redirect_uri;
18use crate::state::AuthState;
19use crate::types::{ClientRegistrationRequest, ClientRegistrationResponse, RegisteredClient};
20use crate::util::{now_unix, random_token, remote_ip};
21
22pub async fn register_client(
23 State(state): State<AuthState>,
24 ConnectInfo(addr): ConnectInfo<SocketAddr>,
25 Json(request): Json<ClientRegistrationRequest>,
26) -> Result<Json<ClientRegistrationResponse>, RegistrationError> {
27 state.check_register_rate_limit(remote_ip(addr)).await?;
28 if request.redirect_uris.is_empty() {
29 warn!("oauth register rejected: no redirect URIs provided");
30 return Err(
31 AuthError::Validation("at least one redirect URI is required".to_string()).into(),
32 );
33 }
34 let native_callback_endpoint = crate::metadata::native_callback_endpoint(&state);
35 for redirect_uri in &request.redirect_uris {
36 if redirect_uri != &native_callback_endpoint
37 && !is_allowed_redirect_uri(redirect_uri, &state.config.allowed_client_redirect_uris)
38 {
39 warn!(
40 redirect_uri = %redirect_uri,
41 native_callback_endpoint = %native_callback_endpoint,
42 allowed_patterns = ?state.config.allowed_client_redirect_uris,
43 "oauth register rejected: redirect URI is not in the allowlist, native callback, or loopback set"
44 );
45 return Err(RegistrationError::InvalidRedirectUri(format!(
46 "redirect URI `{redirect_uri}` must target a loopback host, match the native callback endpoint, or match an allowed redirect pattern"
47 )));
48 }
49 }
50
51 let application_type = match request.application_type.as_deref() {
55 None | Some("web") => "web".to_string(),
56 Some("native") => "native".to_string(),
57 Some(other) => {
58 warn!(
59 application_type = %other,
60 "oauth register rejected: unsupported application_type"
61 );
62 return Err(RegistrationError::InvalidClientMetadata(format!(
63 "application_type `{other}` is not supported; use `web` or `native`"
64 )));
65 }
66 };
67
68 let client = RegisteredClient {
69 client_id: random_token(18)?,
70 redirect_uris: request.redirect_uris,
71 created_at: now_unix(),
72 };
73 state.store.register_client(client.clone()).await?;
74 info!(
75 client_id = %client.client_id,
76 redirect_uri_count = client.redirect_uris.len(),
77 redirect_uris = ?client.redirect_uris,
78 "oauth client registration accepted"
79 );
80 Ok(Json(ClientRegistrationResponse {
81 client_id: client.client_id,
82 redirect_uris: client.redirect_uris,
83 token_endpoint_auth_method: "none".to_string(),
84 application_type,
85 }))
86}
87
88pub enum RegistrationError {
96 InvalidRedirectUri(String),
98 InvalidClientMetadata(String),
101 Auth(AuthError),
107}
108
109impl From<AuthError> for RegistrationError {
110 fn from(error: AuthError) -> Self {
111 Self::Auth(error)
112 }
113}
114
115impl RegistrationError {
116 fn oauth_error(&self) -> &'static str {
117 match self {
118 Self::InvalidRedirectUri(_) => "invalid_redirect_uri",
119 Self::InvalidClientMetadata(_) => "invalid_client_metadata",
120 Self::Auth(AuthError::RateLimited { .. }) => "temporarily_unavailable",
121 Self::Auth(_) => "invalid_client_metadata",
126 }
127 }
128
129 fn log_kind(&self) -> &'static str {
130 match self {
131 Self::InvalidRedirectUri(_) => "invalid_redirect_uri",
132 Self::InvalidClientMetadata(_) => "invalid_client_metadata",
133 Self::Auth(error) => error.kind(),
134 }
135 }
136
137 fn status(&self) -> StatusCode {
144 match self {
145 Self::InvalidRedirectUri(_) | Self::InvalidClientMetadata(_) => StatusCode::BAD_REQUEST,
146 Self::Auth(AuthError::InvalidGrant(_)) => StatusCode::BAD_REQUEST,
147 Self::Auth(AuthError::AuthFailed(_) | AuthError::InvalidAccessToken) => {
148 StatusCode::UNAUTHORIZED
149 }
150 Self::Auth(AuthError::Validation(_)) => StatusCode::UNPROCESSABLE_ENTITY,
151 Self::Auth(AuthError::Network(_) | AuthError::Server(_)) => StatusCode::BAD_GATEWAY,
152 Self::Auth(AuthError::RateLimited { .. }) => StatusCode::TOO_MANY_REQUESTS,
153 Self::Auth(
154 AuthError::Config(_)
155 | AuthError::Storage(_)
156 | AuthError::Decode(_)
157 | AuthError::InsecurePermissions { .. },
158 ) => StatusCode::INTERNAL_SERVER_ERROR,
159 }
160 }
161
162 fn description(&self) -> String {
163 match self {
164 Self::InvalidRedirectUri(message) | Self::InvalidClientMetadata(message) => {
165 message.clone()
166 }
167 Self::Auth(error) => error.to_string(),
168 }
169 }
170
171 fn retry_after_ms(&self) -> Option<u64> {
172 match self {
173 Self::Auth(AuthError::RateLimited { retry_after_ms, .. }) => Some(*retry_after_ms),
174 _ => None,
175 }
176 }
177}
178
179impl IntoResponse for RegistrationError {
180 fn into_response(self) -> Response {
181 let status = self.status();
182 let log_kind = self.log_kind();
183 let retry_after_ms = self.retry_after_ms();
184 let body = Json(serde_json::json!({
185 "error": self.oauth_error(),
186 "error_description": self.description(),
187 }));
188 let mut response = (status, body).into_response();
189 response.extensions_mut().insert(AuthErrorKind(log_kind));
190 response
191 .headers_mut()
192 .insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
193 response
194 .headers_mut()
195 .insert(header::PRAGMA, HeaderValue::from_static("no-cache"));
196 if let Some(retry_after_ms) = retry_after_ms
197 && let Ok(value) = HeaderValue::from_str(&(retry_after_ms / 1_000).max(1).to_string())
198 {
199 response.headers_mut().insert(header::RETRY_AFTER, value);
200 }
201 response
202 }
203}
204
205pub(crate) fn allowlist_redirect_uris(
219 candidate_redirect_uris: &[String],
220 allowed_patterns: &[String],
221) -> Vec<String> {
222 candidate_redirect_uris
223 .iter()
224 .filter(|uri| is_allowed_redirect_uri(uri, allowed_patterns))
225 .cloned()
226 .collect()
227}
228
229pub(crate) fn allowed_uris_from_cimd_document(
237 document: &crate::cimd::document::ClientMetadataDocument,
238 client_id: &str,
239 client_state_id: &str,
240 allowed_patterns: &[String],
241) -> Result<Vec<String>, AuthError> {
242 let allowed = allowlist_redirect_uris(&document.redirect_uris, allowed_patterns);
243 if allowed.is_empty() {
244 warn!(
245 client_id = %client_id,
246 client_state_id = %client_state_id,
247 "oauth authorize rejected: CIMD document declares no allowlisted redirect_uris"
248 );
249 return Err(AuthError::Validation(
250 "client_id metadata document declares no allowed redirect_uris".to_string(),
251 ));
252 }
253 Ok(allowed)
254}
255
256pub(crate) async fn resolve_client_redirect_uris(
262 state: &AuthState,
263 client_id: &str,
264 client_state_id: &str,
265) -> Result<Vec<String>, AuthError> {
266 if crate::cimd::document::is_cimd_client_id(client_id) {
267 let document =
268 crate::cimd::document::fetch_and_validate_client_metadata(&state.cimd_cache, client_id)
269 .await
270 .map_err(|error| {
271 warn!(
272 client_id = %client_id,
273 client_state_id = %client_state_id,
274 kind = error.kind(),
275 error = %error,
276 "oauth authorize rejected: CIMD document fetch/validation failed"
277 );
278 AuthError::Validation(
284 "client_id metadata document is invalid or unreachable".to_string(),
285 )
286 })?;
287 return allowed_uris_from_cimd_document(
288 &document,
289 client_id,
290 client_state_id,
291 &state.config.allowed_client_redirect_uris,
292 );
293 }
294
295 let client = state.store.find_client(client_id).await?.ok_or_else(|| {
296 warn!(
297 client_id = %client_id,
298 client_state_id = %client_state_id,
299 "oauth authorize rejected: unknown client_id"
300 );
301 AuthError::InvalidGrant("unknown client_id".to_string())
302 })?;
303 Ok(client.redirect_uris)
304}