headless_lms_mcp/http/
auth.rs1use actix_web::{
5 Error, HttpMessage, HttpResponse,
6 body::{EitherBody, MessageBody},
7 dev::{ServiceRequest, ServiceResponse},
8 http::header,
9 middleware::Next,
10 web,
11};
12use headless_lms_base::config::ApplicationConfiguration;
13use headless_lms_models::library::oauth::{
14 bearer::{BearerTokenError, TokenSubject, bearer_token},
15 mcp::{McpScope, authenticate_mcp_access_token},
16};
17use serde_json::json;
18
19use super::McpEndpoint;
20use crate::protocol::www_authenticate_challenge;
21
22pub fn protected_resource_metadata_url(app_conf: &ApplicationConfiguration) -> String {
24 app_conf.public_url("/.well-known/oauth-protected-resource/api/v0/mcp")
25}
26
27pub async fn protected_resource_metadata(
32 app_conf: web::Data<ApplicationConfiguration>,
33) -> HttpResponse {
34 HttpResponse::Ok().json(json!({
35 "resource": app_conf.mcp_resource_url(),
36 "authorization_servers": [app_conf.oauth_issuer()],
37 "scopes_supported": McpScope::all_strs(),
38 "bearer_methods_supported": ["header"],
39 "resource_name": "courses.mooc.fi course material",
40 }))
41}
42
43pub async fn authenticate(
47 endpoint: web::Data<McpEndpoint>,
48 req: ServiceRequest,
49 next: Next<impl MessageBody>,
50) -> Result<ServiceResponse<EitherBody<impl MessageBody>>, Error> {
51 let app_conf = &endpoint.state.app_conf;
52 let token = req
53 .headers()
54 .get(header::AUTHORIZATION)
55 .and_then(|value| value.to_str().ok())
56 .and_then(bearer_token)
57 .map(str::to_string);
58 let Some(token) = token else {
59 return Ok(unauthorized(req, app_conf, None));
60 };
61 let mut conn = endpoint
62 .state
63 .pool
64 .acquire()
65 .await
66 .map_err(actix_web::error::ErrorInternalServerError)?;
67 let principal = match authenticate_mcp_access_token(
68 &mut conn,
69 &token,
70 &app_conf.oauth_server_configuration.oauth_token_hmac_key,
71 &app_conf.mcp_resource_url(),
72 )
73 .await
74 {
75 Ok(principal) => principal,
76 Err(BearerTokenError::Invalid(description)) => {
77 return Ok(unauthorized(
78 req,
79 app_conf,
80 Some(("invalid_token", description)),
81 ));
82 }
83 Err(BearerTokenError::Database(err)) => {
84 return Err(actix_web::error::ErrorInternalServerError(err));
85 }
86 };
87 drop(conn);
88 req.extensions_mut().insert(TokenSubject {
89 user_id: principal.actor.user_id,
90 grant_id: principal.actor.oauth_grant_id,
91 });
92 req.extensions_mut().insert(principal);
93 next.call(req)
94 .await
95 .map(ServiceResponse::map_into_left_body)
96}
97
98fn unauthorized<B>(
99 req: ServiceRequest,
100 app_conf: &ApplicationConfiguration,
101 error: Option<(&str, &str)>,
102) -> ServiceResponse<EitherBody<B>> {
103 let response = HttpResponse::Unauthorized()
104 .insert_header((
105 header::WWW_AUTHENTICATE,
106 www_authenticate_challenge(
107 &protected_resource_metadata_url(app_conf),
108 &McpScope::all_strs().join(" "),
109 error,
110 ),
111 ))
112 .finish();
113 req.into_response(response).map_into_right_body()
114}