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