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