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