1 //! OpenID redirect/login API
2 use std
::convert
::TryFrom
;
4 use anyhow
::{bail, format_err, Error}
;
6 use serde_json
::{json, Value}
;
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}
;
12 use proxmox_openid
::{OpenIdAuthenticator, OpenIdConfig}
;
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}
;
20 use crate::server
::ticket
::ApiTicket
;
21 use pbs_config
::CachedUserInfo
;
23 use pbs_config
::open_backup_lockfile
;
25 use crate::auth_helpers
::*;
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(),
33 OpenIdAuthenticator
::discover(&config
, redirect_url
)
41 description
: "OpenId state.",
45 description
: "OpenId authorization code.",
49 description
: "Redirection Url. The client should set this to used server url.",
58 description
: "User name.",
62 description
: "Auth ticket.",
64 CSRFPreventionToken
: {
66 description
: "Cross Site Request Forgery Prevention Token.",
72 permission
: &Permission
::World
,
75 /// Verify OpenID authorization code and create a ticket
77 /// Returns: An authentication ticket with additional infos.
82 rpcenv
: &mut dyn RpcEnvironment
,
83 ) -> Result
<Value
, Error
> {
84 use proxmox_rest_server
::RestEnvironment
;
86 let env
: &RestEnvironment
= rpcenv
.as_any().downcast_ref
::<RestEnvironment
>()
87 .ok_or_else(|| format_err
!("detected worng RpcEnvironment type"))?
;
89 let user_info
= CachedUserInfo
::new()?
;
91 let mut tested_username
= None
;
93 let result
= proxmox
::try_block
!({
95 let (realm
, private_auth_state
) =
96 OpenIdAuthenticator
::verify_public_auth_state(PROXMOX_BACKUP_RUN_DIR_M
!(), &state
)?
;
98 let (domains
, _digest
) = pbs_config
::domains
::config()?
;
99 let config
: OpenIdRealmConfig
= domains
.lookup("openid", &realm
)?
;
101 let open_id
= openid_authenticator(&config
, &redirect_url
)?
;
103 let info
= open_id
.verify_authorization_code(&code
, &private_auth_state
)?
;
105 // eprintln!("VERIFIED {} {:?} {:?}", info.subject().as_str(), info.name(), info.email());
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'"),
115 Some(OpenIdUserAttribute
::Email
) => {
117 Some(name
) => name
.as_str(),
118 None
=> bail
!("missing claim 'email'"),
124 let user_id
= Userid
::try_from(format
!("{}@{}", unique_name
, realm
))?
;
125 tested_username
= Some(unique_name
.to_string());
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)?
;
132 userid
: user_id
.clone(),
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()),
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
);
144 config
.set_data(user
.userid
.as_str(), "user", &user
)?
;
145 user
::save_config(&config
)?
;
147 bail
!("user account '{}' missing, disabled or expired.", user_id
);
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
);
155 env
.log_auth(user_id
.as_str());
160 "CSRFPreventionToken": token
,
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
))
178 schema
: REALM_ID_SCHEMA
,
181 description
: "Redirection Url. The client should set this to used server url.",
187 description
: "Redirection URL.",
191 description
: "Anyone can access this (before the user is authenticated).",
192 permission
: &Permission
::World
,
195 /// Create OpenID Redirect Session
198 redirect_url
: String
,
199 _rpcenv
: &mut dyn RpcEnvironment
,
200 ) -> Result
<String
, Error
> {
202 let (domains
, _digest
) = pbs_config
::domains
::config()?
;
203 let config
: OpenIdRealmConfig
= domains
.lookup("openid", &realm
)?
;
205 let open_id
= openid_authenticator(&config
, &redirect_url
)?
;
207 let url
= open_id
.authorize_url(PROXMOX_BACKUP_RUN_DIR_M
!(), &realm
)?
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
)),
219 pub const ROUTER
: Router
= Router
::new()
220 .get(&list_subdirs_api_method
!(SUBDIRS
))