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