1use crate::client::HttpClient;
6use crate::error::Oid4vciError;
7use crate::IssuerMetadata;
8
9pub async fn discover<C: HttpClient>(
14 client: &C,
15 issuer_url: &str,
16) -> baseid_core::Result<IssuerMetadata> {
17 let url = format!(
18 "{}/.well-known/openid-credential-issuer",
19 issuer_url.trim_end_matches('/')
20 );
21
22 let json = client.get_json(&url).await.map_err(|e| {
23 let err: baseid_core::Error =
24 Oid4vciError::MetadataDiscovery(format!("failed to fetch metadata from {url}: {e}"))
25 .into();
26 err
27 })?;
28
29 serde_json::from_value(json).map_err(|e| {
30 let err: baseid_core::Error =
31 Oid4vciError::MetadataDiscovery(format!("invalid metadata response: {e}")).into();
32 err
33 })
34}
35
36#[cfg(test)]
37mod tests {
38 use super::*;
39 use crate::client::HttpClient;
40 use std::collections::BTreeMap;
41
42 struct MockClient {
43 response: serde_json::Value,
44 }
45
46 impl HttpClient for MockClient {
47 async fn get_json(&self, _url: &str) -> baseid_core::Result<serde_json::Value> {
48 Ok(self.response.clone())
49 }
50 async fn post_form(
51 &self,
52 _url: &str,
53 _params: &[(&str, &str)],
54 ) -> baseid_core::Result<serde_json::Value> {
55 unimplemented!()
56 }
57 async fn post_json_bearer(
58 &self,
59 _url: &str,
60 _body: &serde_json::Value,
61 _token: &str,
62 ) -> baseid_core::Result<serde_json::Value> {
63 unimplemented!()
64 }
65 async fn post_json(
66 &self,
67 _url: &str,
68 _body: &serde_json::Value,
69 ) -> baseid_core::Result<serde_json::Value> {
70 unimplemented!()
71 }
72 async fn get(
73 &self,
74 _url: &str,
75 ) -> baseid_core::Result<baseid_transport::http::HttpResponse> {
76 unimplemented!()
77 }
78 async fn post_raw(
79 &self,
80 _url: &str,
81 _body: &[u8],
82 _content_type: &str,
83 ) -> baseid_core::Result<baseid_transport::http::HttpResponse> {
84 unimplemented!()
85 }
86 }
87
88 #[tokio::test]
89 async fn metadata_discovery() {
90 let client = MockClient {
91 response: serde_json::json!({
92 "credential_issuer": "https://issuer.example.com",
93 "credential_endpoint": "https://issuer.example.com/credential",
94 "credential_configurations_supported": {
95 "UniversityDegree": {
96 "format": "jwt_vc_json",
97 "credential_definition": {
98 "type": ["VerifiableCredential", "UniversityDegreeCredential"]
99 }
100 }
101 }
102 }),
103 };
104
105 let metadata = discover(&client, "https://issuer.example.com")
106 .await
107 .unwrap();
108 assert_eq!(metadata.credential_issuer, "https://issuer.example.com");
109 assert_eq!(
110 metadata.credential_endpoint,
111 "https://issuer.example.com/credential"
112 );
113 assert!(metadata
114 .credential_configurations_supported
115 .contains_key("UniversityDegree"));
116 }
117
118 #[tokio::test]
119 async fn metadata_discovery_with_token_endpoint() {
120 let mut configs = BTreeMap::new();
121 configs.insert("Degree".to_string(), serde_json::json!({}));
122 let client = MockClient {
123 response: serde_json::json!({
124 "credential_issuer": "https://issuer.example.com",
125 "authorization_server": "https://auth.example.com",
126 "credential_endpoint": "https://issuer.example.com/credential",
127 "credential_configurations_supported": configs,
128 }),
129 };
130
131 let metadata = discover(&client, "https://issuer.example.com/")
132 .await
133 .unwrap();
134 assert_eq!(
135 metadata.authorization_server.as_deref(),
136 Some("https://auth.example.com")
137 );
138 }
139
140 struct FailingClient;
141
142 impl HttpClient for FailingClient {
143 async fn get_json(&self, _url: &str) -> baseid_core::Result<serde_json::Value> {
144 Err(baseid_core::error::ProtocolError::Transport.into())
145 }
146 async fn post_form(
147 &self,
148 _url: &str,
149 _params: &[(&str, &str)],
150 ) -> baseid_core::Result<serde_json::Value> {
151 unimplemented!()
152 }
153 async fn post_json_bearer(
154 &self,
155 _url: &str,
156 _body: &serde_json::Value,
157 _token: &str,
158 ) -> baseid_core::Result<serde_json::Value> {
159 unimplemented!()
160 }
161 async fn post_json(
162 &self,
163 _url: &str,
164 _body: &serde_json::Value,
165 ) -> baseid_core::Result<serde_json::Value> {
166 unimplemented!()
167 }
168 async fn get(
169 &self,
170 _url: &str,
171 ) -> baseid_core::Result<baseid_transport::http::HttpResponse> {
172 unimplemented!()
173 }
174 async fn post_raw(
175 &self,
176 _url: &str,
177 _body: &[u8],
178 _content_type: &str,
179 ) -> baseid_core::Result<baseid_transport::http::HttpResponse> {
180 unimplemented!()
181 }
182 }
183
184 #[tokio::test]
185 async fn metadata_discovery_failure() {
186 let result = discover(&FailingClient, "https://issuer.example.com").await;
187 assert!(result.is_err());
188 }
189
190 #[test]
193 fn spec_vector_metadata_jwt_vc_json() {
194 let meta: IssuerMetadata =
195 serde_json::from_str(baseid_test_vectors::oid4vci::METADATA_JWT_VC_JSON).unwrap();
196 assert_eq!(
197 meta.credential_issuer,
198 "https://credential-issuer.example.com"
199 );
200 let config = &meta.credential_configurations_supported["UniversityDegreeCredential"];
201 assert_eq!(config["format"], "jwt_vc_json");
202 assert_eq!(
203 config["credential_signing_alg_values_supported"][0],
204 "ES256"
205 );
206 }
207
208 #[test]
209 fn spec_vector_metadata_sd_jwt_vc() {
210 let meta: IssuerMetadata =
211 serde_json::from_str(baseid_test_vectors::oid4vci::METADATA_SD_JWT_VC).unwrap();
212 let config = &meta.credential_configurations_supported["SD_JWT_VC_example_in_OpenID4VCI"];
213 assert_eq!(config["format"], "dc+sd-jwt");
214 assert_eq!(config["vct"], "SD_JWT_VC_example_in_OpenID4VCI");
215 }
216
217 #[test]
218 fn spec_vector_metadata_mso_mdoc() {
219 let meta: IssuerMetadata =
220 serde_json::from_str(baseid_test_vectors::oid4vci::METADATA_MSO_MDOC).unwrap();
221 let config = &meta.credential_configurations_supported["org.iso.18013.5.1.mDL"];
222 assert_eq!(config["format"], "mso_mdoc");
223 assert_eq!(config["doctype"], "org.iso.18013.5.1.mDL");
224 }
225
226 #[test]
227 fn spec_vector_metadata_full_endpoints() {
228 let meta: IssuerMetadata =
229 serde_json::from_str(baseid_test_vectors::oid4vci::METADATA_FULL).unwrap();
230 assert_eq!(
231 meta.nonce_endpoint.as_deref(),
232 Some("https://credential-issuer.example.com/nonce")
233 );
234 assert_eq!(
235 meta.deferred_credential_endpoint.as_deref(),
236 Some("https://credential-issuer.example.com/deferred_credential")
237 );
238 assert_eq!(
239 meta.notification_endpoint.as_deref(),
240 Some("https://credential-issuer.example.com/notification")
241 );
242 }
243
244 #[test]
245 fn spec_vector_token_response() {
246 use crate::token::TokenResponse;
247 let resp: TokenResponse =
248 serde_json::from_str(baseid_test_vectors::oid4vci::TOKEN_RESPONSE).unwrap();
249 assert_eq!(resp.token_type, "Bearer");
250 assert_eq!(resp.expires_in, Some(86400));
251 }
252
253 #[test]
256 fn spec_interop_metadata_jwt_vc_roundtrip() {
257 let meta: IssuerMetadata =
258 serde_json::from_str(baseid_test_vectors::oid4vci::METADATA_JWT_VC_JSON).unwrap();
259 let json = serde_json::to_string(&meta).unwrap();
260 let reparsed: IssuerMetadata = serde_json::from_str(&json).unwrap();
261 assert_eq!(reparsed.credential_issuer, meta.credential_issuer);
262 assert_eq!(
263 reparsed.credential_configurations_supported.len(),
264 meta.credential_configurations_supported.len()
265 );
266 let config = &reparsed.credential_configurations_supported["UniversityDegreeCredential"];
268 assert_eq!(config["format"], "jwt_vc_json");
269 assert_eq!(
270 config["credential_signing_alg_values_supported"][0],
271 "ES256"
272 );
273 }
274
275 #[test]
276 fn spec_interop_metadata_sd_jwt_vc_roundtrip() {
277 let meta: IssuerMetadata =
278 serde_json::from_str(baseid_test_vectors::oid4vci::METADATA_SD_JWT_VC).unwrap();
279 let json = serde_json::to_string(&meta).unwrap();
280 let reparsed: IssuerMetadata = serde_json::from_str(&json).unwrap();
281 assert_eq!(reparsed.credential_issuer, meta.credential_issuer);
282 assert_eq!(
283 reparsed.credential_configurations_supported.len(),
284 meta.credential_configurations_supported.len()
285 );
286 let config =
287 &reparsed.credential_configurations_supported["SD_JWT_VC_example_in_OpenID4VCI"];
288 assert_eq!(config["format"], "dc+sd-jwt");
289 assert_eq!(config["vct"], "SD_JWT_VC_example_in_OpenID4VCI");
290 }
291
292 #[test]
293 fn spec_interop_metadata_mso_mdoc_roundtrip() {
294 let meta: IssuerMetadata =
295 serde_json::from_str(baseid_test_vectors::oid4vci::METADATA_MSO_MDOC).unwrap();
296 let json = serde_json::to_string(&meta).unwrap();
297 let reparsed: IssuerMetadata = serde_json::from_str(&json).unwrap();
298 assert_eq!(reparsed.credential_issuer, meta.credential_issuer);
299 assert_eq!(
300 reparsed.credential_configurations_supported.len(),
301 meta.credential_configurations_supported.len()
302 );
303 let config = &reparsed.credential_configurations_supported["org.iso.18013.5.1.mDL"];
304 assert_eq!(config["format"], "mso_mdoc");
305 assert_eq!(config["doctype"], "org.iso.18013.5.1.mDL");
306 }
307
308 #[test]
309 fn spec_interop_metadata_full_roundtrip() {
310 let meta: IssuerMetadata =
311 serde_json::from_str(baseid_test_vectors::oid4vci::METADATA_FULL).unwrap();
312 let json = serde_json::to_string(&meta).unwrap();
313 let reparsed: IssuerMetadata = serde_json::from_str(&json).unwrap();
314 assert_eq!(reparsed.credential_issuer, meta.credential_issuer);
315 assert_eq!(reparsed.credential_endpoint, meta.credential_endpoint);
316 assert_eq!(reparsed.nonce_endpoint, meta.nonce_endpoint);
317 assert_eq!(
318 reparsed.deferred_credential_endpoint,
319 meta.deferred_credential_endpoint
320 );
321 assert_eq!(reparsed.notification_endpoint, meta.notification_endpoint);
322 assert_eq!(
323 reparsed.credential_configurations_supported.len(),
324 meta.credential_configurations_supported.len()
325 );
326 }
327}