1 //! OpenID redirect/login API
2 use anyhow
::{bail, format_err, Error}
;
3 use serde_json
::{json, Value}
;
6 http_err
, list_subdirs_api_method
, Permission
, Router
, RpcEnvironment
, SubdirMap
,
8 use proxmox_schema
::api
;
9 use proxmox_sys
::sortable
;
11 use proxmox_openid
::{OpenIdAuthenticator, OpenIdConfig}
;
14 OpenIdRealmConfig
, User
, Userid
, EMAIL_SCHEMA
, FIRST_NAME_SCHEMA
, LAST_NAME_SCHEMA
,
15 OPENID_DEFAILT_SCOPE_LIST
, REALM_ID_SCHEMA
,
17 use pbs_buildcfg
::PROXMOX_BACKUP_RUN_DIR_M
;
18 use pbs_ticket
::Ticket
;
20 use pbs_config
::open_backup_lockfile
;
21 use pbs_config
::CachedUserInfo
;
23 use crate::auth_helpers
::*;
24 use crate::server
::ticket
::ApiTicket
;
26 fn openid_authenticator(
27 realm_config
: &OpenIdRealmConfig
,
29 ) -> Result
<OpenIdAuthenticator
, Error
> {
30 let scopes
: Vec
<String
> = realm_config
33 .unwrap_or(OPENID_DEFAILT_SCOPE_LIST
)
34 .split(|c
: char| c
== '
,'
|| c
== '
;'
|| char::is_ascii_whitespace(&c
))
35 .filter(|s
| !s
.is_empty())
39 let mut acr_values
= None
;
40 if let Some(ref list
) = realm_config
.acr_values
{
42 list
.split(|c
: char| c
== '
,'
|| c
== '
;'
|| char::is_ascii_whitespace(&c
))
43 .filter(|s
| !s
.is_empty())
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(),
57 OpenIdAuthenticator
::discover(&config
, redirect_url
)
64 description
: "OpenId state.",
68 description
: "OpenId authorization code.",
72 description
: "Redirection Url. The client should set this to used server url.",
81 description
: "User name.",
85 description
: "Auth ticket.",
87 CSRFPreventionToken
: {
89 description
: "Cross Site Request Forgery Prevention Token.",
95 permission
: &Permission
::World
,
98 /// Verify OpenID authorization code and create a ticket
100 /// Returns: An authentication ticket with additional infos.
104 redirect_url
: String
,
105 rpcenv
: &mut dyn RpcEnvironment
,
106 ) -> Result
<Value
, Error
> {
107 use proxmox_rest_server
::RestEnvironment
;
109 let env
: &RestEnvironment
= rpcenv
111 .downcast_ref
::<RestEnvironment
>()
112 .ok_or_else(|| format_err
!("detected wrong RpcEnvironment type"))?
;
114 let user_info
= CachedUserInfo
::new()?
;
116 let mut tested_username
= None
;
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
)?
;
122 let (domains
, _digest
) = pbs_config
::domains
::config()?
;
123 let config
: OpenIdRealmConfig
= domains
.lookup("openid", &realm
)?
;
125 let open_id
= openid_authenticator(&config
, &redirect_url
)?
;
127 let info
= open_id
.verify_authorization_code_simple(&code
, &private_auth_state
)?
;
129 // eprintln!("VERIFIED {:?}", info);
131 let name_attr
= config
.username_claim
.as_deref().unwrap_or("sub");
133 // Try to be compatible with previous versions
134 let try_attr
= match name_attr
{
135 "subject" => Some("sub"),
136 "username" => Some("preferred_username"),
140 let unique_name
= match info
[name_attr
].as_str() {
141 Some(name
) => name
.to_owned(),
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
),
149 bail
!("missing claim '{}'", name_attr
);
154 let user_id
= Userid
::try_from(format
!("{}@{}", unique_name
, realm
))?
;
155 tested_username
= Some(unique_name
);
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)?
;
162 let firstname
= info
["given_name"]
164 .map(|n
| n
.to_string())
165 .filter(|n
| FIRST_NAME_SCHEMA
.parse_simple_value(n
).is_ok());
167 let lastname
= info
["family_name"]
169 .map(|n
| n
.to_string())
170 .filter(|n
| LAST_NAME_SCHEMA
.parse_simple_value(n
).is_ok());
172 let email
= info
["email"]
174 .map(|n
| n
.to_string())
175 .filter(|n
| EMAIL_SCHEMA
.parse_simple_value(n
).is_ok());
178 userid
: user_id
.clone(),
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
);
191 bail
!("autocreate user failed - '{}' already exists.", user
.userid
);
194 config
.set_data(user
.userid
.as_str(), "user", &user
)?
;
195 user
::save_config(&config
)?
;
197 bail
!("user account '{}' missing, disabled or expired.", user_id
);
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
);
205 env
.log_auth(user_id
.as_str());
210 "CSRFPreventionToken": token
,
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
));
228 schema
: REALM_ID_SCHEMA
,
231 description
: "Redirection Url. The client should set this to used server url.",
237 description
: "Redirection URL.",
241 description
: "Anyone can access this (before the user is authenticated).",
242 permission
: &Permission
::World
,
245 /// Create OpenID Redirect Session
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
)?
;
254 let open_id
= openid_authenticator(&config
, &redirect_url
)?
;
256 let url
= open_id
.authorize_url(PROXMOX_BACKUP_RUN_DIR_M
!(), &realm
)?
;
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
)),
267 pub const ROUTER
: Router
= Router
::new()
268 .get(&list_subdirs_api_method
!(SUBDIRS
))