]> git.proxmox.com Git - proxmox-openid-rs.git/blob - src/lib.rs
also return data from UserInfo endpoint
[proxmox-openid-rs.git] / src / lib.rs
1 use std::path::Path;
2
3 use anyhow::{format_err, Error};
4 use serde::{Deserialize, Serialize};
5
6 mod http_client;
7 pub use http_client::http_client;
8
9 mod auth_state;
10 pub use auth_state::*;
11
12
13 use openidconnect::{
14 //curl::http_client,
15 core::{
16 CoreProviderMetadata,
17 CoreClient,
18 CoreIdTokenClaims,
19 CoreIdTokenVerifier,
20 CoreAuthenticationFlow,
21 CoreAuthDisplay,
22 CoreAuthPrompt,
23 CoreGenderClaim,
24 },
25 PkceCodeChallenge,
26 PkceCodeVerifier,
27 AuthorizationCode,
28 ClientId,
29 ClientSecret,
30 CsrfToken,
31 IssuerUrl,
32 Nonce,
33 OAuth2TokenResponse,
34 RedirectUrl,
35 Scope,
36 UserInfoClaims,
37 AdditionalClaims,
38 };
39
40 /// Stores Additional Claims into a serde_json::Value;
41 #[derive(Debug, Deserialize, Serialize)]
42 pub struct GenericClaims(serde_json::Value);
43 impl AdditionalClaims for GenericClaims {}
44
45 pub type GenericUserInfoClaims = UserInfoClaims<GenericClaims, CoreGenderClaim>;
46
47 #[derive(Debug, Deserialize, Serialize, Clone)]
48 pub struct OpenIdConfig {
49 pub issuer_url: String,
50 pub client_id: String,
51 #[serde(skip_serializing_if="Option::is_none")]
52 pub client_key: Option<String>,
53 #[serde(skip_serializing_if="Option::is_none")]
54 pub scopes: Option<Vec<String>>,
55 }
56
57 pub struct OpenIdAuthenticator {
58 client: CoreClient,
59 config: OpenIdConfig,
60 }
61
62 #[derive(Debug, Deserialize, Serialize)]
63 pub struct PublicAuthState {
64 pub csrf_token: CsrfToken,
65 pub realm: String,
66 }
67
68 #[derive(Debug, Deserialize, Serialize)]
69 pub struct PrivateAuthState {
70 pub csrf_token: CsrfToken,
71 pub nonce: Nonce,
72 pub pkce_verifier: PkceCodeVerifier,
73 pub ctime: i64,
74 }
75
76 impl PrivateAuthState {
77
78 pub fn new() -> Self {
79 let nonce = Nonce::new_random();
80 let csrf_token = CsrfToken::new_random();
81 let (_pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
82
83 PrivateAuthState {
84 csrf_token,
85 nonce,
86 pkce_verifier,
87 ctime: proxmox_time::epoch_i64(),
88 }
89 }
90
91 pub fn pkce_verifier(&self) -> PkceCodeVerifier {
92 // Note: PkceCodeVerifier does not impl. clone()
93 PkceCodeVerifier::new(self.pkce_verifier.secret().to_string())
94 }
95
96 pub fn pkce_challenge(&self) -> PkceCodeChallenge {
97 PkceCodeChallenge::from_code_verifier_sha256(&self.pkce_verifier)
98 }
99
100 pub fn public_state_string(&self, realm: String) -> Result<String, Error> {
101 let pub_state = PublicAuthState {
102 csrf_token: self.csrf_token.clone(),
103 realm,
104 };
105 Ok(serde_json::to_string(&pub_state)?)
106 }
107 }
108
109 impl OpenIdAuthenticator {
110
111 pub fn discover(config: &OpenIdConfig, redirect_url: &str) -> Result<Self, Error> {
112
113 let client_id = ClientId::new(config.client_id.clone());
114 let client_key = config.client_key.clone().map(|key| ClientSecret::new(key));
115 let issuer_url = IssuerUrl::new(config.issuer_url.clone())?;
116
117 let provider_metadata = CoreProviderMetadata::discover(&issuer_url, http_client)?;
118
119 let client = CoreClient::from_provider_metadata(
120 provider_metadata,
121 client_id,
122 client_key,
123 ).set_redirect_uri(RedirectUrl::new(String::from(redirect_url))?);
124
125 Ok(Self {
126 client,
127 config: config.clone(),
128 })
129 }
130
131 pub fn authorize_url(&self, state_dir: &str, realm: &str) -> Result<String, Error> {
132
133 let private_auth_state = PrivateAuthState::new();
134 let public_auth_state = private_auth_state.public_state_string(realm.to_string())?;
135 let nonce = private_auth_state.nonce.clone();
136
137 store_auth_state(Path::new(state_dir), realm, &private_auth_state)?;
138
139 // Generate the authorization URL to which we'll redirect the user.
140 let mut request = self.client
141 .authorize_url(
142 CoreAuthenticationFlow::AuthorizationCode,
143 || CsrfToken::new(public_auth_state),
144 || nonce,
145 )
146 .set_pkce_challenge(private_auth_state.pkce_challenge());
147
148 request = request.set_display(CoreAuthDisplay::Page);
149
150 request = request.add_prompt(CoreAuthPrompt::Login);
151
152 if let Some(ref scopes) = self.config.scopes {
153 for scope in scopes.clone() {
154 request = request.add_scope(Scope::new(scope));
155 }
156 }
157
158 let (authorize_url, _csrf_state, _nonce) = request.url();
159
160 Ok(authorize_url.to_string())
161 }
162
163 pub fn verify_public_auth_state(
164 state_dir: &str,
165 state: &str,
166 ) -> Result<(String, PrivateAuthState), Error> {
167 verify_public_auth_state(Path::new(state_dir), state)
168 }
169
170 pub fn verify_authorization_code(
171 &self,
172 code: &str,
173 private_auth_state: &PrivateAuthState,
174 ) -> Result<(CoreIdTokenClaims, GenericUserInfoClaims), Error> {
175
176 let code = AuthorizationCode::new(code.to_string());
177 // Exchange the code with a token.
178 let token_response = self.client
179 .exchange_code(code)
180 .set_pkce_verifier(private_auth_state.pkce_verifier())
181 .request(http_client)
182 .map_err(|err| format_err!("Failed to contact token endpoint: {}", err))?;
183
184 let id_token_verifier: CoreIdTokenVerifier = self.client.id_token_verifier();
185 let id_token_claims: &CoreIdTokenClaims = token_response
186 .extra_fields()
187 .id_token()
188 .expect("Server did not return an ID token")
189 .claims(&id_token_verifier, &private_auth_state.nonce)
190 .map_err(|err| format_err!("Failed to verify ID token: {}", err))?;
191
192 let userinfo_claims: GenericUserInfoClaims = self.client
193 .user_info(token_response.access_token().to_owned(), None)?
194 .request(http_client)
195 .map_err(|err| format_err!("Failed to contact userinfo endpoint: {}", err))?;
196
197 Ok((id_token_claims.clone(), userinfo_claims))
198 }
199 }