]> git.proxmox.com Git - pve-access-control.git/blame - src/PVE/API2/OpenId.pm
fix #4074: increase API OpenID code size limit to 2048
[pve-access-control.git] / src / PVE / API2 / OpenId.pm
CommitLineData
ac344d7d
DM
1package PVE::API2::OpenId;
2
3use strict;
4use warnings;
5
6use PVE::Tools qw(extract_param);
7use PVE::RS::OpenId;
8
9use PVE::Exception qw(raise raise_perm_exc raise_param_exc);
10use PVE::SafeSyslog;
11use PVE::RPCEnvironment;
fbf4594a 12use PVE::Cluster qw(cfs_read_file cfs_write_file);
ac344d7d
DM
13use PVE::AccessControl;
14use PVE::JSONSchema qw(get_standard_option);
fbf4594a 15use PVE::Auth::Plugin;
ac344d7d
DM
16
17use PVE::RESTHandler;
18
19use base qw(PVE::RESTHandler);
20
21my $openid_state_path = "/var/lib/pve-manager";
22
23my $lookup_openid_auth = sub {
24 my ($realm, $redirect_url) = @_;
25
26 my $cfg = cfs_read_file('domains.cfg');
27 my $ids = $cfg->{ids};
28
29 die "authentication domain '$realm' does not exist\n" if !$ids->{$realm};
30
31 my $config = $ids->{$realm};
32 die "wrong realm type ($config->{type} != openid)\n" if $config->{type} ne "openid";
33
34 my $openid_config = {
35 issuer_url => $config->{'issuer-url'},
36 client_id => $config->{'client-id'},
37 client_key => $config->{'client-key'},
38 };
348c7038 39 $openid_config->{prompt} = $config->{'prompt'} if defined($config->{'prompt'});
ac344d7d 40
48e51c33
TL
41 my $scopes = $config->{'scopes'} // 'email profile';
42 $openid_config->{scopes} = [ PVE::Tools::split_list($scopes) ];
43
bc9d1159
TL
44 if (defined(my $acr = $config->{'acr-values'})) {
45 $openid_config->{acr_values} = [ PVE::Tools::split_list($acr) ];
46 }
47
ac344d7d
DM
48 my $openid = PVE::RS::OpenId->discover($openid_config, $redirect_url);
49 return ($config, $openid);
50};
51
52__PACKAGE__->register_method ({
53 name => 'index',
54 path => '',
55 method => 'GET',
56 description => "Directory index.",
57 permissions => {
58 user => 'all',
59 },
60 parameters => {
61 additionalProperties => 0,
62 properties => {},
63 },
64 returns => {
65 type => 'array',
66 items => {
67 type => "object",
68 properties => {
69 subdir => { type => 'string' },
70 },
71 },
72 links => [ { rel => 'child', href => "{subdir}" } ],
73 },
74 code => sub {
75 my ($param) = @_;
76
77 return [
78 { subdir => 'auth-url' },
79 { subdir => 'login' },
80 ];
81 }});
82
83__PACKAGE__->register_method ({
84 name => 'auth_url',
85 path => 'auth-url',
86 method => 'POST',
87 protected => 1,
88 description => "Get the OpenId Authorization Url for the specified realm.",
89 parameters => {
90 additionalProperties => 0,
91 properties => {
92 realm => get_standard_option('realm'),
93 'redirect-url' => {
94 description => "Redirection Url. The client should set this to the used server url (location.origin).",
95 type => 'string',
96 maxLength => 255,
97 },
98 },
99 },
100 returns => {
101 type => "string",
102 description => "Redirection URL.",
103 },
104 permissions => { user => 'world' },
105 code => sub {
106 my ($param) = @_;
107
bb0cfca4 108 my $dcconf = PVE::Cluster::cfs_read_file('datacenter.cfg');
afda4f1a 109 local $ENV{all_proxy} = $dcconf->{http_proxy} if exists $dcconf->{http_proxy};
bb0cfca4 110
ac344d7d
DM
111 my $realm = extract_param($param, 'realm');
112 my $redirect_url = extract_param($param, 'redirect-url');
113
114 my ($config, $openid) = $lookup_openid_auth->($realm, $redirect_url);
115 my $url = $openid->authorize_url($openid_state_path , $realm);
116
117 return $url;
118 }});
119
120__PACKAGE__->register_method ({
121 name => 'login',
122 path => 'login',
123 method => 'POST',
124 protected => 1,
125 description => " Verify OpenID authorization code and create a ticket.",
126 parameters => {
127 additionalProperties => 0,
128 properties => {
129 'state' => {
130 description => "OpenId state.",
131 type => 'string',
132 maxLength => 1024,
133 },
134 code => {
135 description => "OpenId authorization code.",
136 type => 'string',
fe52bc63 137 maxLength => 4096,
ac344d7d
DM
138 },
139 'redirect-url' => {
140 description => "Redirection Url. The client should set this to the used server url (location.origin).",
141 type => 'string',
142 maxLength => 255,
143 },
144 },
145 },
146 returns => {
147 properties => {
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 },
153 },
154 },
155 permissions => { user => 'world' },
156 code => sub {
157 my ($param) = @_;
158
159 my $rpcenv = PVE::RPCEnvironment::get();
160
161 my $res;
162 eval {
bb0cfca4 163 my $dcconf = PVE::Cluster::cfs_read_file('datacenter.cfg');
afda4f1a 164 local $ENV{all_proxy} = $dcconf->{http_proxy} if exists $dcconf->{http_proxy};
bb0cfca4 165
ac344d7d
DM
166 my ($realm, $private_auth_state) = PVE::RS::OpenId::verify_public_auth_state(
167 $openid_state_path, $param->{'state'});
168
169 my $redirect_url = extract_param($param, 'redirect-url');
170
171 my ($config, $openid) = $lookup_openid_auth->($realm, $redirect_url);
172
173 my $info = $openid->verify_authorization_code($param->{code}, $private_auth_state);
174 my $subject = $info->{'sub'};
175
271bbc10 176 my $unique_name;
aa71c0f0
TL
177
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;
187 } else {
188 # neither the attr nor fallback are defined in info..
189 die "missing configured claim '$user_attr' in returned info object\n";
ac344d7d
DM
190 }
191
192 my $username = "${unique_name}\@${realm}";
193
fbf4594a
DM
194 # first, check if $username respects our naming conventions
195 PVE::Auth::Plugin::verify_username($username);
196
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");
200
201 die "user '$username' already exists\n" if $usercfg->{users}->{$username};
202
203 my $entry = { enable => 1 };
204 if (defined(my $email = $info->{'email'})) {
205 $entry->{email} = $email;
206 }
207 if (defined(my $given_name = $info->{'given_name'})) {
208 $entry->{firstname} = $given_name;
209 }
210 if (defined(my $family_name = $info->{'family_name'})) {
211 $entry->{lastname} = $family_name;
212 }
213
214 $usercfg->{users}->{$username} = $entry;
215
216 cfs_write_file("user.cfg", $usercfg);
217 }, "autocreate openid user failed");
218 } else {
219 # test if user exists and is enabled
220 $rpcenv->check_user_enabled($username);
221 }
ac344d7d
DM
222
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);
226
227 $res = {
228 ticket => $ticket,
229 username => $username,
230 CSRFPreventionToken => $csrftoken,
231 cap => $cap,
232 };
233
234 my $clinfo = PVE::Cluster::get_clinfo();
235 if ($clinfo->{cluster}->{name} && $rpcenv->check($username, '/', ['Sys.Audit'], 1)) {
236 $res->{clustername} = $clinfo->{cluster}->{name};
237 }
238 };
239 if (my $err = $@) {
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);
244 }
245
246 PVE::Cluster::log_msg('info', 'root@pam', "successful openid auth for user '$res->{username}'");
247
248 return $res;
249 }});