1 package PVE
::API2
::OpenId
;
6 use PVE
::Tools
qw(extract_param);
9 use PVE
::Exception
qw(raise raise_perm_exc raise_param_exc);
11 use PVE
::RPCEnvironment
;
12 use PVE
::Cluster
qw(cfs_read_file cfs_write_file);
13 use PVE
::AccessControl
;
14 use PVE
::JSONSchema
qw(get_standard_option);
15 use PVE
::Auth
::Plugin
;
19 use base
qw(PVE::RESTHandler);
21 my $openid_state_path = "/var/lib/pve-manager";
23 my $lookup_openid_auth = sub {
24 my ($realm, $redirect_url) = @_;
26 my $cfg = cfs_read_file
('domains.cfg');
27 my $ids = $cfg->{ids
};
29 die "authentication domain '$realm' does not exist\n" if !$ids->{$realm};
31 my $config = $ids->{$realm};
32 die "wrong realm type ($config->{type} != openid)\n" if $config->{type
} ne "openid";
35 issuer_url
=> $config->{'issuer-url'},
36 client_id
=> $config->{'client-id'},
37 client_key
=> $config->{'client-key'},
39 $openid_config->{prompt
} = $config->{'prompt'} if defined($config->{'prompt'});
41 my $scopes = $config->{'scopes'} // 'email profile';
42 $openid_config->{scopes
} = [ PVE
::Tools
::split_list
($scopes) ];
44 if (defined(my $acr = $config->{'acr-values'})) {
45 $openid_config->{acr_values
} = [ PVE
::Tools
::split_list
($acr) ];
48 my $openid = PVE
::RS
::OpenId-
>discover($openid_config, $redirect_url);
49 return ($config, $openid);
52 __PACKAGE__-
>register_method ({
56 description
=> "Directory index.",
61 additionalProperties
=> 0,
69 subdir
=> { type
=> 'string' },
72 links
=> [ { rel
=> 'child', href
=> "{subdir}" } ],
78 { subdir
=> 'auth-url' },
79 { subdir
=> 'login' },
83 __PACKAGE__-
>register_method ({
88 description
=> "Get the OpenId Authorization Url for the specified realm.",
90 additionalProperties
=> 0,
92 realm
=> get_standard_option
('realm'),
94 description
=> "Redirection Url. The client should set this to the used server url (location.origin).",
102 description
=> "Redirection URL.",
104 permissions
=> { user
=> 'world' },
108 my $dcconf = PVE
::Cluster
::cfs_read_file
('datacenter.cfg');
109 local $ENV{all_proxy
} = $dcconf->{http_proxy
} if exists $dcconf->{http_proxy
};
111 my $realm = extract_param
($param, 'realm');
112 my $redirect_url = extract_param
($param, 'redirect-url');
114 my ($config, $openid) = $lookup_openid_auth->($realm, $redirect_url);
115 my $url = $openid->authorize_url($openid_state_path , $realm);
120 __PACKAGE__-
>register_method ({
125 description
=> " Verify OpenID authorization code and create a ticket.",
127 additionalProperties
=> 0,
130 description
=> "OpenId state.",
135 description
=> "OpenId authorization code.",
140 description
=> "Redirection Url. The client should set this to the used server url (location.origin).",
148 username
=> { type
=> 'string' },
149 ticket
=> { type
=> 'string' },
150 CSRFPreventionToken
=> { type
=> 'string' },
151 cap
=> { type
=> 'object' }, # computed api permissions
152 clustername
=> { type
=> 'string', optional
=> 1 },
155 permissions
=> { user
=> 'world' },
159 my $rpcenv = PVE
::RPCEnvironment
::get
();
163 my $dcconf = PVE
::Cluster
::cfs_read_file
('datacenter.cfg');
164 local $ENV{all_proxy
} = $dcconf->{http_proxy
} if exists $dcconf->{http_proxy
};
166 my ($realm, $private_auth_state) = PVE
::RS
::OpenId
::verify_public_auth_state
(
167 $openid_state_path, $param->{'state'});
169 my $redirect_url = extract_param
($param, 'redirect-url');
171 my ($config, $openid) = $lookup_openid_auth->($realm, $redirect_url);
173 my $info = $openid->verify_authorization_code($param->{code
}, $private_auth_state);
174 my $subject = $info->{'sub'};
178 my $user_attr = $config->{'username-claim'} // 'sub';
179 if (defined($info->{$user_attr})) {
180 $unique_name = $info->{$user_attr};
181 } elsif ($user_attr eq 'subject') { # stay compat with old versions
182 $unique_name = $subject;
183 } elsif ($user_attr eq 'username') { # stay compat with old versions
184 my $username = $info->{'preferred_username'};
185 die "missing claim 'preferred_username'\n" if !defined($username);
186 $unique_name = $username;
188 # neither the attr nor fallback are defined in info..
189 die "missing configured claim '$user_attr' in returned info object\n";
192 my $username = "${unique_name}\@${realm}";
194 # first, check if $username respects our naming conventions
195 PVE
::Auth
::Plugin
::verify_username
($username);
197 if ($config->{'autocreate'} && !$rpcenv->check_user_exist($username, 1)) {
198 PVE
::AccessControl
::lock_user_config
(sub {
199 my $usercfg = cfs_read_file
("user.cfg");
201 die "user '$username' already exists\n" if $usercfg->{users
}->{$username};
203 my $entry = { enable
=> 1 };
204 if (defined(my $email = $info->{'email'})) {
205 $entry->{email
} = $email;
207 if (defined(my $given_name = $info->{'given_name'})) {
208 $entry->{firstname
} = $given_name;
210 if (defined(my $family_name = $info->{'family_name'})) {
211 $entry->{lastname
} = $family_name;
214 $usercfg->{users
}->{$username} = $entry;
216 cfs_write_file
("user.cfg", $usercfg);
217 }, "autocreate openid user failed");
219 # test if user exists and is enabled
220 $rpcenv->check_user_enabled($username);
223 my $ticket = PVE
::AccessControl
::assemble_ticket
($username);
224 my $csrftoken = PVE
::AccessControl
::assemble_csrf_prevention_token
($username);
225 my $cap = $rpcenv->compute_api_permission($username);
229 username
=> $username,
230 CSRFPreventionToken
=> $csrftoken,
234 my $clinfo = PVE
::Cluster
::get_clinfo
();
235 if ($clinfo->{cluster
}->{name
} && $rpcenv->check($username, '/', ['Sys.Audit'], 1)) {
236 $res->{clustername
} = $clinfo->{cluster
}->{name
};
240 my $clientip = $rpcenv->get_client_ip() || '';
241 syslog
('err', "openid authentication failure; rhost=$clientip msg=$err");
242 # do not return any info to prevent user enumeration attacks
243 die PVE
::Exception-
>new("authentication failure\n", code
=> 401);
246 PVE
::Cluster
::log_msg
('info', 'root@pam', "successful openid auth for user '$res->{username}'");