]> git.proxmox.com Git - pve-access-control.git/blame - src/PVE/API2/OpenId.pm
api: implement openid API
[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;
12use PVE::Cluster qw(cfs_read_file);
13use PVE::AccessControl;
14use PVE::JSONSchema qw(get_standard_option);
15
16use PVE::RESTHandler;
17
18use base qw(PVE::RESTHandler);
19
20my $openid_state_path = "/var/lib/pve-manager";
21
22my $lookup_openid_auth = sub {
23 my ($realm, $redirect_url) = @_;
24
25 my $cfg = cfs_read_file('domains.cfg');
26 my $ids = $cfg->{ids};
27
28 die "authentication domain '$realm' does not exist\n" if !$ids->{$realm};
29
30 my $config = $ids->{$realm};
31 die "wrong realm type ($config->{type} != openid)\n" if $config->{type} ne "openid";
32
33 my $openid_config = {
34 issuer_url => $config->{'issuer-url'},
35 client_id => $config->{'client-id'},
36 client_key => $config->{'client-key'},
37 };
38
39 my $openid = PVE::RS::OpenId->discover($openid_config, $redirect_url);
40 return ($config, $openid);
41};
42
43__PACKAGE__->register_method ({
44 name => 'index',
45 path => '',
46 method => 'GET',
47 description => "Directory index.",
48 permissions => {
49 user => 'all',
50 },
51 parameters => {
52 additionalProperties => 0,
53 properties => {},
54 },
55 returns => {
56 type => 'array',
57 items => {
58 type => "object",
59 properties => {
60 subdir => { type => 'string' },
61 },
62 },
63 links => [ { rel => 'child', href => "{subdir}" } ],
64 },
65 code => sub {
66 my ($param) = @_;
67
68 return [
69 { subdir => 'auth-url' },
70 { subdir => 'login' },
71 ];
72 }});
73
74__PACKAGE__->register_method ({
75 name => 'auth_url',
76 path => 'auth-url',
77 method => 'POST',
78 protected => 1,
79 description => "Get the OpenId Authorization Url for the specified realm.",
80 parameters => {
81 additionalProperties => 0,
82 properties => {
83 realm => get_standard_option('realm'),
84 'redirect-url' => {
85 description => "Redirection Url. The client should set this to the used server url (location.origin).",
86 type => 'string',
87 maxLength => 255,
88 },
89 },
90 },
91 returns => {
92 type => "string",
93 description => "Redirection URL.",
94 },
95 permissions => { user => 'world' },
96 code => sub {
97 my ($param) = @_;
98
99 my $realm = extract_param($param, 'realm');
100 my $redirect_url = extract_param($param, 'redirect-url');
101
102 my ($config, $openid) = $lookup_openid_auth->($realm, $redirect_url);
103 my $url = $openid->authorize_url($openid_state_path , $realm);
104
105 return $url;
106 }});
107
108__PACKAGE__->register_method ({
109 name => 'login',
110 path => 'login',
111 method => 'POST',
112 protected => 1,
113 description => " Verify OpenID authorization code and create a ticket.",
114 parameters => {
115 additionalProperties => 0,
116 properties => {
117 'state' => {
118 description => "OpenId state.",
119 type => 'string',
120 maxLength => 1024,
121 },
122 code => {
123 description => "OpenId authorization code.",
124 type => 'string',
125 maxLength => 1024,
126 },
127 'redirect-url' => {
128 description => "Redirection Url. The client should set this to the used server url (location.origin).",
129 type => 'string',
130 maxLength => 255,
131 },
132 },
133 },
134 returns => {
135 properties => {
136 username => { type => 'string' },
137 ticket => { type => 'string' },
138 CSRFPreventionToken => { type => 'string' },
139 cap => { type => 'object' }, # computed api permissions
140 clustername => { type => 'string', optional => 1 },
141 },
142 },
143 permissions => { user => 'world' },
144 code => sub {
145 my ($param) = @_;
146
147 my $rpcenv = PVE::RPCEnvironment::get();
148
149 my $res;
150 eval {
151 my ($realm, $private_auth_state) = PVE::RS::OpenId::verify_public_auth_state(
152 $openid_state_path, $param->{'state'});
153
154 my $redirect_url = extract_param($param, 'redirect-url');
155
156 my ($config, $openid) = $lookup_openid_auth->($realm, $redirect_url);
157
158 my $info = $openid->verify_authorization_code($param->{code}, $private_auth_state);
159 my $subject = $info->{'sub'};
160
161 die "missing openid claim 'sub'\n" if !defined($subject);
162
163 my $unique_name = $subject; # default
164 if (defined(my $user_attr = $config->{'username-claim'})) {
165 if ($user_attr eq 'subject') {
166 $unique_name = $subject;
167 } elsif ($user_attr eq 'username') {
168 my $username = $info->{'preferred_username'};
169 die "missing claim 'preferred_username'\n" if !defined($username);
170 $unique_name = $username;
171 } elsif ($user_attr eq 'email') {
172 my $email = $info->{'email'};
173 die "missing claim 'email'\n" if !defined($email);
174 $unique_name = $email;
175 } else {
176 die "got unexpected value for 'username-claim': '${user_attr}'\n";
177 }
178 }
179
180 my $username = "${unique_name}\@${realm}";
181
182 # test if user exists and is enabled
183 $rpcenv->check_user_enabled($username);
184
185 my $ticket = PVE::AccessControl::assemble_ticket($username);
186 my $csrftoken = PVE::AccessControl::assemble_csrf_prevention_token($username);
187 my $cap = $rpcenv->compute_api_permission($username);
188
189 $res = {
190 ticket => $ticket,
191 username => $username,
192 CSRFPreventionToken => $csrftoken,
193 cap => $cap,
194 };
195
196 my $clinfo = PVE::Cluster::get_clinfo();
197 if ($clinfo->{cluster}->{name} && $rpcenv->check($username, '/', ['Sys.Audit'], 1)) {
198 $res->{clustername} = $clinfo->{cluster}->{name};
199 }
200 };
201 if (my $err = $@) {
202 my $clientip = $rpcenv->get_client_ip() || '';
203 syslog('err', "openid authentication failure; rhost=$clientip msg=$err");
204 # do not return any info to prevent user enumeration attacks
205 die PVE::Exception->new("authentication failure\n", code => 401);
206 }
207
208 PVE::Cluster::log_msg('info', 'root@pam', "successful openid auth for user '$res->{username}'");
209
210 return $res;
211 }});