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