1use crate::{
2 errors::PlatformError,
3 stores::platform_store::{PlatformData, PlatformStore},
4};
5use async_trait::async_trait;
6use cached::proc_macro::cached;
7use jsonwebtoken::jwk::JwkSet;
8use phf::phf_map;
9use reqwest::{header, Client};
10use serde::{Deserialize, Serialize};
11use tokio::time::Duration;
12
13pub const USER_AGENT: &str =
14 "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible;) LTI JWK Requester";
15
16pub const CANVAS_PUBLIC_JWKS_URL: &str = "https://sso.canvaslms.com/api/lti/security/jwks";
17pub const CANVAS_OIDC_URL: &str = "https://sso.canvaslms.com/api/lti/authorize_redirect";
18pub const CANVAS_AUTH_TOKEN_URL: &str = "https://sso.canvaslms.com/login/oauth2/token";
19
20pub const CANVAS_BETA_PUBLIC_JWKS_URL: &str =
21 "https://sso.beta.canvaslms.com/api/lti/security/jwks";
22pub const CANVAS_BETA_AUTH_TOKEN_URL: &str = "https://sso.beta.canvaslms.com/login/oauth2/token";
23pub const CANVAS_BETA_OIDC_URL: &str = "https://sso.beta.canvaslms.com/api/lti/authorize_redirect";
24
25pub const CANVAS_SUBMISSION_TYPE: &str = "https://canvas.instructure.com/lti/submission_type";
26
27#[derive(Debug, Deserialize, Serialize)]
28pub struct Platform<'a> {
29 pub iss: &'a str,
30 pub jwks_url: &'a str,
31 pub token_url: &'a str,
32 pub oidc_url: &'a str,
33}
34
35static PLATFORMS: phf::Map<&'static str, Platform> = phf_map! {
36 "https://canvas.instructure.com" =>
37 Platform {
38 iss: "https://canvas.instructure.com",
39 jwks_url: CANVAS_PUBLIC_JWKS_URL,
40 token_url: CANVAS_AUTH_TOKEN_URL,
41 oidc_url: CANVAS_OIDC_URL,
42 },
43 "https://canvas.beta.instructure.com" =>
44 Platform {
45 iss: "https://canvas.beta.instructure.com",
46 jwks_url: CANVAS_BETA_PUBLIC_JWKS_URL,
47 token_url: CANVAS_BETA_AUTH_TOKEN_URL,
48 oidc_url: CANVAS_BETA_OIDC_URL,
49 },
50 "https://schoology.schoology.com" =>
51 Platform {
52 iss: "https://schoology.schoology.com",
53 jwks_url: "https://lti-service.svc.schoology.com/lti-service/.well-known/jwks",
54 token_url: "https://lti-service.svc.schoology.com/lti-service/access-token",
55 oidc_url: "https://lti-service.svc.schoology.com/lti-service/authorize-redirect",
56 },
57 "https://ltiadvantagevalidator.imsglobal.org" =>
58 Platform {
59 iss: "https://ltiadvantagevalidator.imsglobal.org",
60 jwks_url: "https://oauth2server.imsglobal.org/jwks",
61 token_url: "https://ltiadvantagevalidator.imsglobal.org/ltitool/authcodejwt.html",
62 oidc_url: "https://ltiadvantagevalidator.imsglobal.org/ltitool/oidcauthurl.html",
63 },
64 "https://build.1edtech.org" =>
65 Platform {
66 iss: "https://build.1edtech.org",
67 jwks_url: "https://build.1edtech.org/jwks",
68 token_url: "https://build.1edtech.org/auth",
69 oidc_url: "https://build.1edtech.org/oidc",
70 },
71 "https://lms.example.com" =>
72 Platform {
73 iss: "https://lms.example.com",
74 jwks_url: "https://lms.example.com/jwks",
75 token_url: "https://lms.example.com/auth",
76 oidc_url: "https://lms.example.com/oidc",
77 },
78};
79
80pub struct StaticPlatformStore<'a> {
81 pub iss: &'a str,
82}
83
84#[async_trait]
85impl PlatformStore for StaticPlatformStore<'_> {
86 async fn get_jwk_server_url(&self) -> Result<String, PlatformError> {
87 let platform = self.get_platform()?;
88 Ok(platform.jwks_url.to_string())
89 }
90
91 async fn get_oidc_url(&self) -> Result<String, PlatformError> {
92 let platform = self.get_platform()?;
93 Ok(platform.oidc_url.to_string())
94 }
95
96 async fn get_token_url(&self) -> Result<String, PlatformError> {
97 let platform = self.get_platform()?;
98 Ok(platform.token_url.to_string())
99 }
100
101 async fn create(&self, _platform: PlatformData) -> Result<PlatformData, PlatformError> {
102 Err(PlatformError::UnsupportedOperation(
103 "StaticPlatformStore does not support create operations".to_string(),
104 ))
105 }
106
107 async fn find_by_iss(&self, issuer: &str) -> Result<Option<PlatformData>, PlatformError> {
108 match PLATFORMS.get(issuer) {
109 Some(platform) => Ok(Some(PlatformData {
110 issuer: platform.iss.to_string(),
111 name: None,
112 jwks_url: platform.jwks_url.to_string(),
113 token_url: platform.token_url.to_string(),
114 oidc_url: platform.oidc_url.to_string(),
115 })),
116 None => Ok(None),
117 }
118 }
119
120 async fn update(
121 &self,
122 _issuer: &str,
123 _platform: PlatformData,
124 ) -> Result<PlatformData, PlatformError> {
125 Err(PlatformError::UnsupportedOperation(
126 "StaticPlatformStore does not support update operations".to_string(),
127 ))
128 }
129
130 async fn delete(&self, _issuer: &str) -> Result<(), PlatformError> {
131 Err(PlatformError::UnsupportedOperation(
132 "StaticPlatformStore does not support delete operations".to_string(),
133 ))
134 }
135
136 async fn list(&self) -> Result<Vec<PlatformData>, PlatformError> {
137 let platforms: Vec<PlatformData> = PLATFORMS
138 .values()
139 .map(|p| PlatformData {
140 issuer: p.iss.to_string(),
141 name: None,
142 jwks_url: p.jwks_url.to_string(),
143 token_url: p.token_url.to_string(),
144 oidc_url: p.oidc_url.to_string(),
145 })
146 .collect();
147 Ok(platforms)
148 }
149}
150
151impl StaticPlatformStore<'_> {
152 fn get_platform(&self) -> Result<&Platform, PlatformError> {
153 let platform = PLATFORMS
154 .get(self.iss)
155 .ok_or(PlatformError::InvalidIss(self.iss.to_string()))?;
156 Ok(platform)
157 }
158}
159
160#[cached(
161 time = 3600, result = true, sync_writes = "default", )]
165pub async fn get_jwk_set(jwk_server_url: String) -> Result<JwkSet, PlatformError> {
166 let client = Client::new();
167 let resp = client
168 .get(jwk_server_url)
169 .header(header::USER_AGENT, USER_AGENT)
170 .send()
171 .await
172 .map_err(|e| PlatformError::JWKSRequestFailed(e.to_string()))?;
173
174 let jwks_resp = resp
175 .text()
176 .await
177 .map_err(|e| PlatformError::JWKSRequestFailed(e.to_string()))?;
178
179 let jwks: JwkSet = serde_json::from_str(&jwks_resp)
180 .map_err(|e| PlatformError::JWKSRequestFailed(e.to_string()))?;
181
182 Ok(jwks)
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 const TEST_STORE: StaticPlatformStore = StaticPlatformStore {
190 iss: "https://lms.example.com",
191 };
192
193 const INVALID_TEST_STORE: StaticPlatformStore = StaticPlatformStore {
194 iss: "https://invalid.com",
195 };
196
197 #[test]
198 fn test_get_platform() {
199 let result = TEST_STORE.get_platform();
200 assert!(result.is_ok());
201
202 let platform = result.unwrap();
203 assert_eq!(platform.iss, "https://lms.example.com");
204 assert_eq!(platform.jwks_url, "https://lms.example.com/jwks");
205 assert_eq!(platform.oidc_url, "https://lms.example.com/oidc");
206 }
207
208 #[tokio::test]
209 async fn test_get_jwk_server_url() {
210 let result = TEST_STORE.get_jwk_server_url().await;
211 assert!(result.is_ok());
212
213 let jwk_server_url = result.unwrap();
214 assert_eq!(jwk_server_url, "https://lms.example.com/jwks");
215 }
216
217 #[tokio::test]
218 async fn test_get_oidc_url() {
219 let result = TEST_STORE.get_oidc_url().await;
220 assert!(result.is_ok());
221
222 let platform_oidc_url = result.unwrap();
223 assert_eq!(platform_oidc_url, "https://lms.example.com/oidc");
224 }
225
226 #[test]
227 fn test_get_platform_invalid_iss() {
228 let result = INVALID_TEST_STORE.get_platform();
229 assert!(result.is_err());
230
231 let error = result.unwrap_err();
232 assert_eq!(
233 error,
234 PlatformError::InvalidIss("https://invalid.com".to_string())
235 );
236 }
237
238 #[tokio::test]
239 async fn test_get_jwk_server_url_invalid_iss() {
240 let result = INVALID_TEST_STORE.get_jwk_server_url().await;
241 assert!(result.is_err());
242
243 let error = result.unwrap_err();
244 assert_eq!(
245 error,
246 PlatformError::InvalidIss("https://invalid.com".to_string())
247 );
248 }
249
250 #[tokio::test]
251 async fn test_get_oidc_url_invalid_iss() {
252 let result = INVALID_TEST_STORE.get_oidc_url().await;
253 assert!(result.is_err());
254
255 let error = result.unwrap_err();
256 assert_eq!(
257 error,
258 PlatformError::InvalidIss("https://invalid.com".to_string())
259 );
260 }
261}