]> git.proxmox.com Git - pve-access-control.git/blob - src/PVE/API2/OpenId.pm
fix #3513: pass configured proxy to OpenID
[pve-access-control.git] / src / PVE / API2 / OpenId.pm
1 package PVE::API2::OpenId;
2
3 use strict;
4 use warnings;
5
6 use PVE::Tools qw(extract_param);
7 use PVE::RS::OpenId;
8
9 use PVE::Exception qw(raise raise_perm_exc raise_param_exc);
10 use PVE::SafeSyslog;
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;
16
17 use PVE::RESTHandler;
18
19 use base qw(PVE::RESTHandler);
20
21 my $openid_state_path = "/var/lib/pve-manager";
22
23 my $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 };
39
40 my $openid = PVE::RS::OpenId->discover($openid_config, $redirect_url);
41 return ($config, $openid);
42 };
43
44 __PACKAGE__->register_method ({
45 name => 'index',
46 path => '',
47 method => 'GET',
48 description => "Directory index.",
49 permissions => {
50 user => 'all',
51 },
52 parameters => {
53 additionalProperties => 0,
54 properties => {},
55 },
56 returns => {
57 type => 'array',
58 items => {
59 type => "object",
60 properties => {
61 subdir => { type => 'string' },
62 },
63 },
64 links => [ { rel => 'child', href => "{subdir}" } ],
65 },
66 code => sub {
67 my ($param) = @_;
68
69 return [
70 { subdir => 'auth-url' },
71 { subdir => 'login' },
72 ];
73 }});
74
75 __PACKAGE__->register_method ({
76 name => 'auth_url',
77 path => 'auth-url',
78 method => 'POST',
79 protected => 1,
80 description => "Get the OpenId Authorization Url for the specified realm.",
81 parameters => {
82 additionalProperties => 0,
83 properties => {
84 realm => get_standard_option('realm'),
85 'redirect-url' => {
86 description => "Redirection Url. The client should set this to the used server url (location.origin).",
87 type => 'string',
88 maxLength => 255,
89 },
90 },
91 },
92 returns => {
93 type => "string",
94 description => "Redirection URL.",
95 },
96 permissions => { user => 'world' },
97 code => sub {
98 my ($param) = @_;
99
100 my $dcconf = PVE::Cluster::cfs_read_file('datacenter.cfg');
101 local $ENV{all_proxy} = $dcconf->{http_proxy};
102
103 my $realm = extract_param($param, 'realm');
104 my $redirect_url = extract_param($param, 'redirect-url');
105
106 my ($config, $openid) = $lookup_openid_auth->($realm, $redirect_url);
107 my $url = $openid->authorize_url($openid_state_path , $realm);
108
109 return $url;
110 }});
111
112 __PACKAGE__->register_method ({
113 name => 'login',
114 path => 'login',
115 method => 'POST',
116 protected => 1,
117 description => " Verify OpenID authorization code and create a ticket.",
118 parameters => {
119 additionalProperties => 0,
120 properties => {
121 'state' => {
122 description => "OpenId state.",
123 type => 'string',
124 maxLength => 1024,
125 },
126 code => {
127 description => "OpenId authorization code.",
128 type => 'string',
129 maxLength => 1024,
130 },
131 'redirect-url' => {
132 description => "Redirection Url. The client should set this to the used server url (location.origin).",
133 type => 'string',
134 maxLength => 255,
135 },
136 },
137 },
138 returns => {
139 properties => {
140 username => { type => 'string' },
141 ticket => { type => 'string' },
142 CSRFPreventionToken => { type => 'string' },
143 cap => { type => 'object' }, # computed api permissions
144 clustername => { type => 'string', optional => 1 },
145 },
146 },
147 permissions => { user => 'world' },
148 code => sub {
149 my ($param) = @_;
150
151 my $rpcenv = PVE::RPCEnvironment::get();
152
153 my $res;
154 eval {
155 my $dcconf = PVE::Cluster::cfs_read_file('datacenter.cfg');
156 local $ENV{all_proxy} = $dcconf->{http_proxy};
157
158 my ($realm, $private_auth_state) = PVE::RS::OpenId::verify_public_auth_state(
159 $openid_state_path, $param->{'state'});
160
161 my $redirect_url = extract_param($param, 'redirect-url');
162
163 my ($config, $openid) = $lookup_openid_auth->($realm, $redirect_url);
164
165 my $info = $openid->verify_authorization_code($param->{code}, $private_auth_state);
166 my $subject = $info->{'sub'};
167
168 die "missing openid claim 'sub'\n" if !defined($subject);
169
170 my $unique_name = $subject; # default
171 if (defined(my $user_attr = $config->{'username-claim'})) {
172 if ($user_attr eq 'subject') {
173 $unique_name = $subject;
174 } elsif ($user_attr eq 'username') {
175 my $username = $info->{'preferred_username'};
176 die "missing claim 'preferred_username'\n" if !defined($username);
177 $unique_name = $username;
178 } elsif ($user_attr eq 'email') {
179 my $email = $info->{'email'};
180 die "missing claim 'email'\n" if !defined($email);
181 $unique_name = $email;
182 } else {
183 die "got unexpected value for 'username-claim': '${user_attr}'\n";
184 }
185 }
186
187 my $username = "${unique_name}\@${realm}";
188
189 # first, check if $username respects our naming conventions
190 PVE::Auth::Plugin::verify_username($username);
191
192 if ($config->{'autocreate'} && !$rpcenv->check_user_exist($username, 1)) {
193 PVE::AccessControl::lock_user_config(sub {
194 my $usercfg = cfs_read_file("user.cfg");
195
196 die "user '$username' already exists\n" if $usercfg->{users}->{$username};
197
198 my $entry = { enable => 1 };
199 if (defined(my $email = $info->{'email'})) {
200 $entry->{email} = $email;
201 }
202 if (defined(my $given_name = $info->{'given_name'})) {
203 $entry->{firstname} = $given_name;
204 }
205 if (defined(my $family_name = $info->{'family_name'})) {
206 $entry->{lastname} = $family_name;
207 }
208
209 $usercfg->{users}->{$username} = $entry;
210
211 cfs_write_file("user.cfg", $usercfg);
212 }, "autocreate openid user failed");
213 } else {
214 # test if user exists and is enabled
215 $rpcenv->check_user_enabled($username);
216 }
217
218 my $ticket = PVE::AccessControl::assemble_ticket($username);
219 my $csrftoken = PVE::AccessControl::assemble_csrf_prevention_token($username);
220 my $cap = $rpcenv->compute_api_permission($username);
221
222 $res = {
223 ticket => $ticket,
224 username => $username,
225 CSRFPreventionToken => $csrftoken,
226 cap => $cap,
227 };
228
229 my $clinfo = PVE::Cluster::get_clinfo();
230 if ($clinfo->{cluster}->{name} && $rpcenv->check($username, '/', ['Sys.Audit'], 1)) {
231 $res->{clustername} = $clinfo->{cluster}->{name};
232 }
233 };
234 if (my $err = $@) {
235 my $clientip = $rpcenv->get_client_ip() || '';
236 syslog('err', "openid authentication failure; rhost=$clientip msg=$err");
237 # do not return any info to prevent user enumeration attacks
238 die PVE::Exception->new("authentication failure\n", code => 401);
239 }
240
241 PVE::Cluster::log_msg('info', 'root@pam', "successful openid auth for user '$res->{username}'");
242
243 return $res;
244 }});