]>
Commit | Line | Data |
---|---|---|
3b7b1dfb DM |
1 | //! OpenID redirect/login API |
2 | use std::convert::TryFrom; | |
3 | ||
36b7085e | 4 | use anyhow::{bail, format_err, Error}; |
3b7b1dfb DM |
5 | use serde_json::{json, Value}; |
6 | ||
e25982f2 | 7 | use proxmox_sys::sortable; |
6ef1b649 WB |
8 | use proxmox_router::{ |
9 | http_err, list_subdirs_api_method, Router, RpcEnvironment, SubdirMap, Permission, | |
10 | }; | |
9fa3026a | 11 | use proxmox_schema::api; |
3b7b1dfb | 12 | |
10beed11 | 13 | use proxmox_openid::{OpenIdAuthenticator, OpenIdConfig}; |
26a3450f | 14 | |
10beed11 DM |
15 | use pbs_api_types::{ |
16 | OpenIdRealmConfig, User, Userid, | |
17 | EMAIL_SCHEMA, FIRST_NAME_SCHEMA, LAST_NAME_SCHEMA, OPENID_DEFAILT_SCOPE_LIST, | |
18 | REALM_ID_SCHEMA, | |
19 | }; | |
923f94a4 | 20 | use pbs_buildcfg::PROXMOX_BACKUP_RUN_DIR_M; |
9eb78407 | 21 | use pbs_tools::ticket::Ticket; |
3b7b1dfb | 22 | |
ba3d7e19 | 23 | use pbs_config::CachedUserInfo; |
21211748 | 24 | use pbs_config::open_backup_lockfile; |
7526d864 | 25 | |
3b7b1dfb | 26 | use crate::auth_helpers::*; |
01a08021 | 27 | use crate::server::ticket::ApiTicket; |
3b7b1dfb | 28 | |
26a3450f | 29 | fn openid_authenticator(realm_config: &OpenIdRealmConfig, redirect_url: &str) -> Result<OpenIdAuthenticator, Error> { |
10beed11 DM |
30 | |
31 | let scopes: Vec<String> = realm_config.scopes.as_deref().unwrap_or(OPENID_DEFAILT_SCOPE_LIST) | |
32 | .split(|c: char| c == ',' || c == ';' || char::is_ascii_whitespace(&c)) | |
33 | .filter(|s| !s.is_empty()) | |
34 | .map(String::from) | |
35 | .collect(); | |
36 | ||
37 | let mut acr_values = None; | |
38 | if let Some(ref list) = realm_config.acr_values { | |
39 | acr_values = Some( | |
40 | list | |
41 | .split(|c: char| c == ',' || c == ';' || char::is_ascii_whitespace(&c)) | |
42 | .filter(|s| !s.is_empty()) | |
43 | .map(String::from) | |
44 | .collect() | |
45 | ); | |
46 | } | |
47 | ||
26a3450f FG |
48 | let config = OpenIdConfig { |
49 | issuer_url: realm_config.issuer_url.clone(), | |
50 | client_id: realm_config.client_id.clone(), | |
51 | client_key: realm_config.client_key.clone(), | |
10beed11 DM |
52 | prompt: realm_config.prompt.clone(), |
53 | scopes: Some(scopes), | |
54 | acr_values, | |
26a3450f FG |
55 | }; |
56 | OpenIdAuthenticator::discover(&config, redirect_url) | |
57 | } | |
58 | ||
3b7b1dfb DM |
59 | #[api( |
60 | input: { | |
61 | properties: { | |
62 | state: { | |
63 | description: "OpenId state.", | |
64 | type: String, | |
65 | }, | |
66 | code: { | |
67 | description: "OpenId authorization code.", | |
68 | type: String, | |
69 | }, | |
70 | "redirect-url": { | |
71 | description: "Redirection Url. The client should set this to used server url.", | |
72 | type: String, | |
73 | }, | |
74 | }, | |
75 | }, | |
76 | returns: { | |
77 | properties: { | |
78 | username: { | |
79 | type: String, | |
80 | description: "User name.", | |
81 | }, | |
82 | ticket: { | |
83 | type: String, | |
84 | description: "Auth ticket.", | |
85 | }, | |
86 | CSRFPreventionToken: { | |
87 | type: String, | |
88 | description: "Cross Site Request Forgery Prevention Token.", | |
89 | }, | |
90 | }, | |
91 | }, | |
92 | protected: true, | |
93 | access: { | |
94 | permission: &Permission::World, | |
95 | }, | |
96 | )] | |
97 | /// Verify OpenID authorization code and create a ticket | |
98 | /// | |
99 | /// Returns: An authentication ticket with additional infos. | |
100 | pub fn openid_login( | |
101 | state: String, | |
102 | code: String, | |
103 | redirect_url: String, | |
36b7085e | 104 | rpcenv: &mut dyn RpcEnvironment, |
3b7b1dfb | 105 | ) -> Result<Value, Error> { |
36b7085e DM |
106 | use proxmox_rest_server::RestEnvironment; |
107 | ||
108 | let env: &RestEnvironment = rpcenv.as_any().downcast_ref::<RestEnvironment>() | |
109 | .ok_or_else(|| format_err!("detected worng RpcEnvironment type"))?; | |
110 | ||
3b7b1dfb DM |
111 | let user_info = CachedUserInfo::new()?; |
112 | ||
36b7085e DM |
113 | let mut tested_username = None; |
114 | ||
6ef1b649 | 115 | let result = proxmox_lang::try_block!({ |
3b7b1dfb | 116 | |
36b7085e DM |
117 | let (realm, private_auth_state) = |
118 | OpenIdAuthenticator::verify_public_auth_state(PROXMOX_BACKUP_RUN_DIR_M!(), &state)?; | |
3b7b1dfb | 119 | |
36b7085e DM |
120 | let (domains, _digest) = pbs_config::domains::config()?; |
121 | let config: OpenIdRealmConfig = domains.lookup("openid", &realm)?; | |
3b7b1dfb | 122 | |
36b7085e | 123 | let open_id = openid_authenticator(&config, &redirect_url)?; |
3b7b1dfb | 124 | |
10beed11 | 125 | let info = open_id.verify_authorization_code_simple(&code, &private_auth_state)?; |
3b7b1dfb | 126 | |
10beed11 | 127 | // eprintln!("VERIFIED {:?}", info); |
36b7085e | 128 | |
10beed11 DM |
129 | let name_attr = config.username_claim.as_deref().unwrap_or("sub"); |
130 | ||
131 | // Try to be compatible with previous versions | |
132 | let try_attr = match name_attr { | |
133 | "subject" => Some("sub"), | |
134 | "username" => Some("preferred_username"), | |
135 | _ => None, | |
136 | }; | |
137 | ||
138 | let unique_name = match info[name_attr].as_str() { | |
139 | Some(name) => name.to_owned(), | |
140 | None => { | |
141 | if let Some(try_attr) = try_attr { | |
142 | match info[try_attr].as_str() { | |
143 | Some(name) => name.to_owned(), | |
144 | None => bail!("missing claim '{}'", name_attr), | |
145 | } | |
146 | } else { | |
147 | bail!("missing claim '{}'", name_attr); | |
36b7085e | 148 | } |
3b7b1dfb | 149 | } |
36b7085e DM |
150 | }; |
151 | ||
36b7085e DM |
152 | let user_id = Userid::try_from(format!("{}@{}", unique_name, realm))?; |
153 | tested_username = Some(unique_name.to_string()); | |
154 | ||
155 | if !user_info.is_active_user_id(&user_id) { | |
156 | if config.autocreate.unwrap_or(false) { | |
157 | use pbs_config::user; | |
158 | let _lock = open_backup_lockfile(user::USER_CFG_LOCKFILE, None, true)?; | |
68fd9ca6 | 159 | |
10beed11 | 160 | let firstname = info["given_name"].as_str().map(|n| n.to_string()) |
9fa3026a | 161 | .filter(|n| FIRST_NAME_SCHEMA.parse_simple_value(n).is_ok()); |
68fd9ca6 | 162 | |
10beed11 | 163 | let lastname = info["family_name"].as_str().map(|n| n.to_string()) |
9fa3026a | 164 | .filter(|n| LAST_NAME_SCHEMA.parse_simple_value(n).is_ok()); |
68fd9ca6 | 165 | |
10beed11 | 166 | let email = info["email"].as_str().map(|n| n.to_string()) |
9fa3026a | 167 | .filter(|n| EMAIL_SCHEMA.parse_simple_value(n).is_ok()); |
68fd9ca6 | 168 | |
36b7085e DM |
169 | let user = User { |
170 | userid: user_id.clone(), | |
171 | comment: None, | |
172 | enable: None, | |
173 | expire: None, | |
68fd9ca6 DM |
174 | firstname, |
175 | lastname, | |
176 | email, | |
36b7085e DM |
177 | }; |
178 | let (mut config, _digest) = user::config()?; | |
689ed513 DM |
179 | if let Ok(old_user) = config.lookup::<User>("user", user.userid.as_str()) { |
180 | if let Some(false) = old_user.enable { | |
181 | bail!("user '{}' is disabled.", user.userid); | |
182 | } else { | |
183 | bail!("autocreate user failed - '{}' already exists.", user.userid); | |
184 | } | |
36b7085e DM |
185 | } |
186 | config.set_data(user.userid.as_str(), "user", &user)?; | |
187 | user::save_config(&config)?; | |
188 | } else { | |
189 | bail!("user account '{}' missing, disabled or expired.", user_id); | |
3b7b1dfb | 190 | } |
3b7b1dfb | 191 | } |
3b7b1dfb | 192 | |
36b7085e DM |
193 | let api_ticket = ApiTicket::full(user_id.clone()); |
194 | let ticket = Ticket::new("PBS", &api_ticket)?.sign(private_auth_key(), None)?; | |
195 | let token = assemble_csrf_prevention_token(csrf_secret(), &user_id); | |
3b7b1dfb | 196 | |
36b7085e DM |
197 | env.log_auth(user_id.as_str()); |
198 | ||
199 | Ok(json!({ | |
200 | "username": user_id, | |
201 | "ticket": ticket, | |
202 | "CSRFPreventionToken": token, | |
203 | })) | |
204 | }); | |
205 | ||
206 | if let Err(ref err) = result { | |
207 | let msg = err.to_string(); | |
208 | env.log_failed_auth(tested_username, &msg); | |
209 | return Err(http_err!(UNAUTHORIZED, "{}", msg)) | |
210 | } | |
3b7b1dfb | 211 | |
36b7085e | 212 | result |
3b7b1dfb DM |
213 | } |
214 | ||
215 | #[api( | |
216 | protected: true, | |
217 | input: { | |
218 | properties: { | |
219 | realm: { | |
220 | schema: REALM_ID_SCHEMA, | |
221 | }, | |
222 | "redirect-url": { | |
223 | description: "Redirection Url. The client should set this to used server url.", | |
224 | type: String, | |
225 | }, | |
226 | }, | |
227 | }, | |
228 | returns: { | |
229 | description: "Redirection URL.", | |
230 | type: String, | |
231 | }, | |
232 | access: { | |
233 | description: "Anyone can access this (before the user is authenticated).", | |
234 | permission: &Permission::World, | |
235 | }, | |
236 | )] | |
237 | /// Create OpenID Redirect Session | |
238 | fn openid_auth_url( | |
239 | realm: String, | |
240 | redirect_url: String, | |
241 | _rpcenv: &mut dyn RpcEnvironment, | |
242 | ) -> Result<String, Error> { | |
243 | ||
21211748 | 244 | let (domains, _digest) = pbs_config::domains::config()?; |
3b7b1dfb DM |
245 | let config: OpenIdRealmConfig = domains.lookup("openid", &realm)?; |
246 | ||
26a3450f | 247 | let open_id = openid_authenticator(&config, &redirect_url)?; |
3b7b1dfb DM |
248 | |
249 | let url = open_id.authorize_url(PROXMOX_BACKUP_RUN_DIR_M!(), &realm)? | |
250 | .to_string(); | |
251 | ||
252 | Ok(url.into()) | |
253 | } | |
254 | ||
255 | #[sortable] | |
256 | const SUBDIRS: SubdirMap = &sorted!([ | |
257 | ("login", &Router::new().post(&API_METHOD_OPENID_LOGIN)), | |
258 | ("auth-url", &Router::new().post(&API_METHOD_OPENID_AUTH_URL)), | |
259 | ]); | |
260 | ||
261 | pub const ROUTER: Router = Router::new() | |
262 | .get(&list_subdirs_api_method!(SUBDIRS)) | |
263 | .subdirs(SUBDIRS); |