| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
package Plack::Auth::SSO::OIDC; |
|
2
|
|
|
|
|
|
|
|
|
3
|
1
|
|
|
1
|
|
101378
|
use strict; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
27
|
|
|
4
|
1
|
|
|
1
|
|
4
|
use warnings; |
|
|
1
|
|
|
|
|
5
|
|
|
|
1
|
|
|
|
|
48
|
|
|
5
|
1
|
|
|
1
|
|
4
|
use feature qw(:5.10); |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
132
|
|
|
6
|
1
|
|
|
1
|
|
396
|
use Data::Util qw(:check); |
|
|
1
|
|
|
|
|
912
|
|
|
|
1
|
|
|
|
|
236
|
|
|
7
|
1
|
|
|
1
|
|
357
|
use Data::UUID; |
|
|
1
|
|
|
|
|
601
|
|
|
|
1
|
|
|
|
|
56
|
|
|
8
|
1
|
|
|
1
|
|
1733
|
use Moo; |
|
|
1
|
|
|
|
|
6852
|
|
|
|
1
|
|
|
|
|
5
|
|
|
9
|
1
|
|
|
1
|
|
2094
|
use Plack::Request; |
|
|
1
|
|
|
|
|
95487
|
|
|
|
1
|
|
|
|
|
50
|
|
|
10
|
1
|
|
|
1
|
|
609
|
use Plack::Session; |
|
|
1
|
|
|
|
|
1674
|
|
|
|
1
|
|
|
|
|
81
|
|
|
11
|
1
|
|
|
1
|
|
9
|
use URI; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
29
|
|
|
12
|
1
|
|
|
1
|
|
870
|
use LWP::UserAgent; |
|
|
1
|
|
|
|
|
62436
|
|
|
|
1
|
|
|
|
|
53
|
|
|
13
|
1
|
|
|
1
|
|
966
|
use JSON; |
|
|
1
|
|
|
|
|
12428
|
|
|
|
1
|
|
|
|
|
8
|
|
|
14
|
1
|
|
|
1
|
|
1255
|
use Crypt::JWT; |
|
|
1
|
|
|
|
|
66723
|
|
|
|
1
|
|
|
|
|
85
|
|
|
15
|
1
|
|
|
1
|
|
812
|
use MIME::Base64; |
|
|
1
|
|
|
|
|
994
|
|
|
|
1
|
|
|
|
|
85
|
|
|
16
|
1
|
|
|
1
|
|
760
|
use Digest::SHA; |
|
|
1
|
|
|
|
|
4538
|
|
|
|
1
|
|
|
|
|
77
|
|
|
17
|
1
|
|
|
1
|
|
14
|
use Try::Tiny; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
4586
|
|
|
18
|
|
|
|
|
|
|
|
|
19
|
|
|
|
|
|
|
our $VERSION = "0.015"; |
|
20
|
|
|
|
|
|
|
|
|
21
|
|
|
|
|
|
|
with "Plack::Auth::SSO"; |
|
22
|
|
|
|
|
|
|
|
|
23
|
|
|
|
|
|
|
has scope => ( |
|
24
|
|
|
|
|
|
|
is => "lazy", |
|
25
|
|
|
|
|
|
|
isa => sub { |
|
26
|
|
|
|
|
|
|
is_string($_[0]) or die("scope should be string"); |
|
27
|
|
|
|
|
|
|
index($_[0], "openid") >= 0 or die("default scope openid not included"); |
|
28
|
|
|
|
|
|
|
}, |
|
29
|
|
|
|
|
|
|
default => sub { "openid profile email" }, |
|
30
|
|
|
|
|
|
|
required => 1 |
|
31
|
|
|
|
|
|
|
); |
|
32
|
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
has client_id => ( |
|
34
|
|
|
|
|
|
|
is => "ro", |
|
35
|
|
|
|
|
|
|
isa => sub { is_string($_[0]) or die("client_id should be string"); }, |
|
36
|
|
|
|
|
|
|
required => 1 |
|
37
|
|
|
|
|
|
|
); |
|
38
|
|
|
|
|
|
|
|
|
39
|
|
|
|
|
|
|
has client_secret => ( |
|
40
|
|
|
|
|
|
|
is => "ro", |
|
41
|
|
|
|
|
|
|
isa => sub { is_string($_[0]) or die("client_secret should be string"); }, |
|
42
|
|
|
|
|
|
|
required => 1 |
|
43
|
|
|
|
|
|
|
); |
|
44
|
|
|
|
|
|
|
|
|
45
|
|
|
|
|
|
|
has openid_uri => ( |
|
46
|
|
|
|
|
|
|
is => "ro", |
|
47
|
|
|
|
|
|
|
isa => sub { is_string($_[0]) or die("openid_uri should be string"); }, |
|
48
|
|
|
|
|
|
|
required => 1 |
|
49
|
|
|
|
|
|
|
); |
|
50
|
|
|
|
|
|
|
|
|
51
|
|
|
|
|
|
|
has uid_key => ( |
|
52
|
|
|
|
|
|
|
is => "ro", |
|
53
|
|
|
|
|
|
|
isa => sub { is_string($_[0]) or die("uid_key should be string"); }, |
|
54
|
|
|
|
|
|
|
required => 1 |
|
55
|
|
|
|
|
|
|
); |
|
56
|
|
|
|
|
|
|
|
|
57
|
|
|
|
|
|
|
has authorize_params => ( |
|
58
|
|
|
|
|
|
|
is => "ro", |
|
59
|
|
|
|
|
|
|
isa => sub { is_hash_ref($_[0]) or die("authorize_params should be hash reference"); }, |
|
60
|
|
|
|
|
|
|
lazy => 1, |
|
61
|
|
|
|
|
|
|
default => sub { +{}; }, |
|
62
|
|
|
|
|
|
|
required => 1 |
|
63
|
|
|
|
|
|
|
); |
|
64
|
|
|
|
|
|
|
|
|
65
|
|
|
|
|
|
|
has allowed_authorize_params => ( |
|
66
|
|
|
|
|
|
|
is => "ro", |
|
67
|
|
|
|
|
|
|
isa => sub { is_array_ref($_[0]) or die("allowed_authorize_params should be array reference"); }, |
|
68
|
|
|
|
|
|
|
lazy => 1, |
|
69
|
|
|
|
|
|
|
default => sub { []; }, |
|
70
|
|
|
|
|
|
|
required => 1 |
|
71
|
|
|
|
|
|
|
); |
|
72
|
|
|
|
|
|
|
|
|
73
|
|
|
|
|
|
|
has store_oidc_response => ( |
|
74
|
|
|
|
|
|
|
is => "ro", |
|
75
|
|
|
|
|
|
|
default => sub { 1; }, |
|
76
|
|
|
|
|
|
|
); |
|
77
|
|
|
|
|
|
|
|
|
78
|
|
|
|
|
|
|
|
|
79
|
|
|
|
|
|
|
# internal (non overwritable) moo attributes |
|
80
|
|
|
|
|
|
|
has json => ( |
|
81
|
|
|
|
|
|
|
is => "ro", |
|
82
|
|
|
|
|
|
|
lazy => 1, |
|
83
|
|
|
|
|
|
|
default => sub { |
|
84
|
|
|
|
|
|
|
JSON->new->utf8(1); |
|
85
|
|
|
|
|
|
|
}, |
|
86
|
|
|
|
|
|
|
init_arg => undef |
|
87
|
|
|
|
|
|
|
); |
|
88
|
|
|
|
|
|
|
|
|
89
|
|
|
|
|
|
|
has ua => ( |
|
90
|
|
|
|
|
|
|
is => "ro", |
|
91
|
|
|
|
|
|
|
lazy => 1, |
|
92
|
|
|
|
|
|
|
default => sub { |
|
93
|
|
|
|
|
|
|
LWP::UserAgent->new(); |
|
94
|
|
|
|
|
|
|
}, |
|
95
|
|
|
|
|
|
|
init_arg => undef |
|
96
|
|
|
|
|
|
|
); |
|
97
|
|
|
|
|
|
|
|
|
98
|
|
|
|
|
|
|
has openid_configuration => ( |
|
99
|
|
|
|
|
|
|
is => "lazy", |
|
100
|
|
|
|
|
|
|
init_arg => undef |
|
101
|
|
|
|
|
|
|
); |
|
102
|
|
|
|
|
|
|
|
|
103
|
|
|
|
|
|
|
has jwks => ( |
|
104
|
|
|
|
|
|
|
is => "lazy", |
|
105
|
|
|
|
|
|
|
init_arg => undef |
|
106
|
|
|
|
|
|
|
); |
|
107
|
|
|
|
|
|
|
|
|
108
|
|
|
|
|
|
|
sub get_json { |
|
109
|
|
|
|
|
|
|
|
|
110
|
0
|
|
|
0
|
0
|
|
my ($self, $url) = @_; |
|
111
|
|
|
|
|
|
|
|
|
112
|
0
|
|
|
|
|
|
my $res = $self->ua->get($url); |
|
113
|
|
|
|
|
|
|
|
|
114
|
0
|
0
|
|
|
|
|
if ( $res->code ne "200" ) { |
|
115
|
|
|
|
|
|
|
|
|
116
|
0
|
|
|
|
|
|
$self->log->errorf("url $url returned invalid status code %s", $res->code); |
|
117
|
0
|
|
|
|
|
|
return undef, "INVALID_HTTP_STATUS"; |
|
118
|
|
|
|
|
|
|
|
|
119
|
|
|
|
|
|
|
} |
|
120
|
|
|
|
|
|
|
|
|
121
|
0
|
0
|
|
|
|
|
if ( index($res->content_type, "json") < 0 ) { |
|
122
|
|
|
|
|
|
|
|
|
123
|
0
|
|
|
|
|
|
$self->log->errorf("url $url returned invalid content type %s", $res->content_type); |
|
124
|
0
|
|
|
|
|
|
return undef, "INVALID_HTTP_CONTENT_TYPE"; |
|
125
|
|
|
|
|
|
|
|
|
126
|
|
|
|
|
|
|
} |
|
127
|
|
|
|
|
|
|
|
|
128
|
0
|
|
|
|
|
|
my $data; |
|
129
|
|
|
|
|
|
|
my $data_error; |
|
130
|
|
|
|
|
|
|
try { |
|
131
|
0
|
|
|
0
|
|
|
$data = $self->json->decode($res->content); |
|
132
|
|
|
|
|
|
|
} catch { |
|
133
|
0
|
|
|
0
|
|
|
$data_error = $_; |
|
134
|
0
|
|
|
|
|
|
}; |
|
135
|
|
|
|
|
|
|
|
|
136
|
0
|
0
|
|
|
|
|
if ( defined($data_error) ) { |
|
137
|
|
|
|
|
|
|
|
|
138
|
0
|
|
|
|
|
|
$self->log->error("could not decode json returned from $url"); |
|
139
|
0
|
|
|
|
|
|
return undef, "INVALID_HTTP_CONTENT"; |
|
140
|
|
|
|
|
|
|
|
|
141
|
|
|
|
|
|
|
} |
|
142
|
|
|
|
|
|
|
|
|
143
|
0
|
|
|
|
|
|
$data; |
|
144
|
|
|
|
|
|
|
|
|
145
|
|
|
|
|
|
|
} |
|
146
|
|
|
|
|
|
|
|
|
147
|
|
|
|
|
|
|
sub _build_openid_configuration { |
|
148
|
|
|
|
|
|
|
|
|
149
|
0
|
|
|
0
|
|
|
my $self = $_[0]; |
|
150
|
|
|
|
|
|
|
|
|
151
|
0
|
|
|
|
|
|
my $url = $self->openid_uri; |
|
152
|
0
|
|
|
|
|
|
my ($data, @errors) = $self->get_json($url); |
|
153
|
0
|
0
|
|
|
|
|
die("unable to retrieve openid configuration from $url: ".join(", ",@errors)) |
|
154
|
|
|
|
|
|
|
unless defined($data); |
|
155
|
0
|
|
|
|
|
|
$data; |
|
156
|
|
|
|
|
|
|
|
|
157
|
|
|
|
|
|
|
} |
|
158
|
|
|
|
|
|
|
|
|
159
|
|
|
|
|
|
|
# https://auth0.com/blog/navigating-rs256-and-jwks/ |
|
160
|
|
|
|
|
|
|
sub _build_jwks { |
|
161
|
|
|
|
|
|
|
|
|
162
|
0
|
|
|
0
|
|
|
my $self = $_[0]; |
|
163
|
|
|
|
|
|
|
|
|
164
|
0
|
|
|
|
|
|
my $jwks_uri = $self->openid_configuration->{jwks_uri}; |
|
165
|
|
|
|
|
|
|
|
|
166
|
0
|
0
|
|
|
|
|
die("attribute jwks_uri not found in openid_configuration") |
|
167
|
|
|
|
|
|
|
unless is_string($jwks_uri); |
|
168
|
|
|
|
|
|
|
|
|
169
|
0
|
|
|
|
|
|
my ($data, @errors) = $self->get_json($jwks_uri); |
|
170
|
|
|
|
|
|
|
|
|
171
|
0
|
0
|
|
|
|
|
die("unable to retrieve jwks from ".$self->openid_configuration->{jwks_uri}.":".join(", ", @errors)) |
|
172
|
|
|
|
|
|
|
if scalar(@errors); |
|
173
|
|
|
|
|
|
|
|
|
174
|
0
|
|
|
|
|
|
$data; |
|
175
|
|
|
|
|
|
|
|
|
176
|
|
|
|
|
|
|
} |
|
177
|
|
|
|
|
|
|
|
|
178
|
|
|
|
|
|
|
sub redirect_uri { |
|
179
|
|
|
|
|
|
|
|
|
180
|
0
|
|
|
0
|
1
|
|
my ($self, $request) = @_; |
|
181
|
0
|
|
|
|
|
|
my $redirect_uri = $self->uri_base().$request->request_uri(); |
|
182
|
0
|
|
|
|
|
|
my $idx = index( $redirect_uri, "?" ); |
|
183
|
0
|
0
|
|
|
|
|
if ( $idx >= 0 ) { |
|
184
|
|
|
|
|
|
|
|
|
185
|
0
|
|
|
|
|
|
$redirect_uri = substr( $redirect_uri, 0, $idx ); |
|
186
|
|
|
|
|
|
|
|
|
187
|
|
|
|
|
|
|
} |
|
188
|
0
|
|
|
|
|
|
$redirect_uri; |
|
189
|
|
|
|
|
|
|
|
|
190
|
|
|
|
|
|
|
} |
|
191
|
|
|
|
|
|
|
|
|
192
|
|
|
|
|
|
|
sub make_random_string { |
|
193
|
|
|
|
|
|
|
|
|
194
|
0
|
|
|
0
|
0
|
|
MIME::Base64::encode_base64url( |
|
195
|
|
|
|
|
|
|
Data::UUID->new->create() . |
|
196
|
|
|
|
|
|
|
Data::UUID->new->create() . |
|
197
|
|
|
|
|
|
|
Data::UUID->new->create() |
|
198
|
|
|
|
|
|
|
); |
|
199
|
|
|
|
|
|
|
|
|
200
|
|
|
|
|
|
|
} |
|
201
|
|
|
|
|
|
|
|
|
202
|
|
|
|
|
|
|
sub generate_authorization_uri { |
|
203
|
|
|
|
|
|
|
|
|
204
|
0
|
|
|
0
|
0
|
|
my ($self, %args) = @_; |
|
205
|
|
|
|
|
|
|
|
|
206
|
0
|
|
|
|
|
|
my $request = $args{request}; |
|
207
|
0
|
|
|
|
|
|
my $session = $args{session}; |
|
208
|
0
|
|
|
|
|
|
my $query_params = $request->query_parameters(); |
|
209
|
|
|
|
|
|
|
|
|
210
|
0
|
|
|
|
|
|
my $openid_conf = $self->openid_configuration; |
|
211
|
0
|
|
|
|
|
|
my $authorization_endpoint = $openid_conf->{authorization_endpoint}; |
|
212
|
|
|
|
|
|
|
|
|
213
|
|
|
|
|
|
|
# cf. https://developers.onelogin.com/openid-connect/guides/auth-flow-pkce |
|
214
|
|
|
|
|
|
|
# Note: minimum of 43 characters! |
|
215
|
0
|
|
|
|
|
|
my $code_verifier = $self->make_random_string(); |
|
216
|
0
|
|
|
|
|
|
my $code_challenge = MIME::Base64::encode_base64url(Digest::SHA::sha256($code_verifier),""); |
|
217
|
0
|
|
|
|
|
|
my $state = $self->make_random_string(); |
|
218
|
|
|
|
|
|
|
|
|
219
|
0
|
|
|
|
|
|
my %query; |
|
220
|
|
|
|
|
|
|
|
|
221
|
|
|
|
|
|
|
# merge in allowed params from current url |
|
222
|
0
|
|
|
|
|
|
for my $key( @{ $self->allowed_authorize_params } ){ |
|
|
0
|
|
|
|
|
|
|
|
223
|
|
|
|
|
|
|
|
|
224
|
0
|
|
|
|
|
|
my $val = $query_params->get($key); |
|
225
|
|
|
|
|
|
|
|
|
226
|
0
|
0
|
|
|
|
|
next unless is_string($val); |
|
227
|
|
|
|
|
|
|
|
|
228
|
0
|
|
|
|
|
|
$query{$key} = $val; |
|
229
|
|
|
|
|
|
|
|
|
230
|
|
|
|
|
|
|
} |
|
231
|
|
|
|
|
|
|
|
|
232
|
|
|
|
|
|
|
%query = ( |
|
233
|
|
|
|
|
|
|
%query, |
|
234
|
0
|
|
|
|
|
|
%{ $self->authorize_params }, |
|
|
0
|
|
|
|
|
|
|
|
235
|
|
|
|
|
|
|
code_challenge => $code_challenge, |
|
236
|
|
|
|
|
|
|
code_challenge_method => "S256", |
|
237
|
|
|
|
|
|
|
state => $state, |
|
238
|
|
|
|
|
|
|
scope => $self->scope(), |
|
239
|
|
|
|
|
|
|
client_id => $self->client_id, |
|
240
|
|
|
|
|
|
|
response_type => "code", |
|
241
|
|
|
|
|
|
|
redirect_uri => $self->redirect_uri($request) |
|
242
|
|
|
|
|
|
|
); |
|
243
|
|
|
|
|
|
|
|
|
244
|
0
|
|
|
|
|
|
my $uri = URI->new($authorization_endpoint); |
|
245
|
0
|
|
|
|
|
|
$uri->query_form(%query); |
|
246
|
0
|
|
|
|
|
|
$self->set_csrf_token($session, $state); |
|
247
|
0
|
|
|
|
|
|
$session->set("auth_sso_oidc_code_verifier", $code_verifier); |
|
248
|
|
|
|
|
|
|
|
|
249
|
0
|
|
|
|
|
|
$uri->as_string; |
|
250
|
|
|
|
|
|
|
|
|
251
|
|
|
|
|
|
|
} |
|
252
|
|
|
|
|
|
|
|
|
253
|
|
|
|
|
|
|
around cleanup => sub { |
|
254
|
|
|
|
|
|
|
|
|
255
|
|
|
|
|
|
|
my ($orig, $self, $session) = @_; |
|
256
|
|
|
|
|
|
|
$self->$orig($session); |
|
257
|
|
|
|
|
|
|
$session->remove("auth_sso_oidc_code_verifier"); |
|
258
|
|
|
|
|
|
|
|
|
259
|
|
|
|
|
|
|
}; |
|
260
|
|
|
|
|
|
|
|
|
261
|
|
|
|
|
|
|
# extract_claims_from_id_token(id_token) : claims_as_hash |
|
262
|
|
|
|
|
|
|
sub extract_claims_from_id_token { |
|
263
|
|
|
|
|
|
|
|
|
264
|
0
|
|
|
0
|
0
|
|
my ($self, $id_token) = @_; |
|
265
|
|
|
|
|
|
|
|
|
266
|
0
|
|
|
|
|
|
my ($jose_header, $payload, $s) = split(/\./o, $id_token); |
|
267
|
|
|
|
|
|
|
|
|
268
|
|
|
|
|
|
|
# '{ "alg": "RS256", "kid": "my-key-id" }' |
|
269
|
0
|
|
|
|
|
|
$jose_header = $self->json->decode(MIME::Base64::decode($jose_header)); |
|
270
|
|
|
|
|
|
|
|
|
271
|
|
|
|
|
|
|
#{ "keys": [{ "kid": "my-key-id", "alg": "RS256", "use": "sig" .. }] } |
|
272
|
0
|
|
|
|
|
|
my $jwks = $self->jwks(); |
|
273
|
0
|
|
|
|
|
|
my ($key) = grep { $_->{kid} eq $jose_header->{kid} } |
|
274
|
0
|
|
|
|
|
|
@{ $jwks->{keys} }; |
|
|
0
|
|
|
|
|
|
|
|
275
|
|
|
|
|
|
|
|
|
276
|
0
|
|
|
|
|
|
my $claims; |
|
277
|
|
|
|
|
|
|
my $claims_error; |
|
278
|
|
|
|
|
|
|
try { |
|
279
|
0
|
|
|
0
|
|
|
$claims = Crypt::JWT::decode_jwt(token => $id_token, key => $key); |
|
280
|
|
|
|
|
|
|
} catch { |
|
281
|
0
|
|
|
0
|
|
|
$claims_error = $_; |
|
282
|
0
|
|
|
|
|
|
}; |
|
283
|
|
|
|
|
|
|
|
|
284
|
0
|
0
|
|
|
|
|
$self->log->errorf("error occurred while decoding JWS: %s", $claims_error) |
|
285
|
|
|
|
|
|
|
if defined $claims_error; |
|
286
|
|
|
|
|
|
|
|
|
287
|
0
|
|
|
|
|
|
$claims; |
|
288
|
|
|
|
|
|
|
|
|
289
|
|
|
|
|
|
|
} |
|
290
|
|
|
|
|
|
|
|
|
291
|
|
|
|
|
|
|
sub exchange_code_for_tokens { |
|
292
|
|
|
|
|
|
|
|
|
293
|
0
|
|
|
0
|
0
|
|
my ($self, %args) = @_; |
|
294
|
|
|
|
|
|
|
|
|
295
|
0
|
|
|
|
|
|
my $request = $args{request}; |
|
296
|
0
|
|
|
|
|
|
my $session = $args{session}; |
|
297
|
0
|
|
|
|
|
|
my $code = $args{code}; |
|
298
|
|
|
|
|
|
|
|
|
299
|
0
|
|
|
|
|
|
my $openid_conf = $self->openid_configuration; |
|
300
|
0
|
|
|
|
|
|
my $token_endpoint = $openid_conf->{token_endpoint}; |
|
301
|
0
|
|
0
|
|
|
|
my $token_endpoint_auth_methods_supported = $openid_conf->{token_endpoint_auth_methods_supported} // []; |
|
302
|
0
|
0
|
|
|
|
|
$token_endpoint_auth_methods_supported = |
|
303
|
|
|
|
|
|
|
is_array_ref($token_endpoint_auth_methods_supported) ? |
|
304
|
|
|
|
|
|
|
$token_endpoint_auth_methods_supported : |
|
305
|
|
|
|
|
|
|
[$token_endpoint_auth_methods_supported]; |
|
306
|
|
|
|
|
|
|
|
|
307
|
0
|
|
|
|
|
|
my $auth_sso_oidc_code_verifier = $session->get("auth_sso_oidc_code_verifier"); |
|
308
|
|
|
|
|
|
|
|
|
309
|
0
|
|
|
|
|
|
my $params = { |
|
310
|
|
|
|
|
|
|
grant_type => "authorization_code", |
|
311
|
|
|
|
|
|
|
client_id => $self->client_id, |
|
312
|
|
|
|
|
|
|
code => $code, |
|
313
|
|
|
|
|
|
|
code_verifier => $auth_sso_oidc_code_verifier, |
|
314
|
|
|
|
|
|
|
redirect_uri => $self->redirect_uri($request), |
|
315
|
|
|
|
|
|
|
}; |
|
316
|
|
|
|
|
|
|
|
|
317
|
0
|
|
|
|
|
|
my $headers = { |
|
318
|
|
|
|
|
|
|
"Content-Type" => "application/x-www-form-urlencoded" |
|
319
|
|
|
|
|
|
|
}; |
|
320
|
0
|
|
|
|
|
|
my $client_id = $self->client_id; |
|
321
|
0
|
|
|
|
|
|
my $client_secret = $self->client_secret; |
|
322
|
|
|
|
|
|
|
|
|
323
|
0
|
0
|
|
|
|
|
if ( grep { $_ eq "client_secret_basic" } @$token_endpoint_auth_methods_supported ) { |
|
|
0
|
0
|
|
|
|
|
|
|
324
|
|
|
|
|
|
|
|
|
325
|
0
|
|
|
|
|
|
$self->log->info("using client_secret_basic"); |
|
326
|
0
|
|
|
|
|
|
$headers->{"Authorization"} = "Basic " . MIME::Base64::encode("$client_id:$client_secret", ""); |
|
327
|
|
|
|
|
|
|
|
|
328
|
|
|
|
|
|
|
} |
|
329
|
0
|
|
|
|
|
|
elsif ( grep { $_ eq "client_secret_post" } @$token_endpoint_auth_methods_supported ) { |
|
330
|
|
|
|
|
|
|
|
|
331
|
0
|
|
|
|
|
|
$self->log->info("using client_secret_post"); |
|
332
|
0
|
|
|
|
|
|
$params->{client_secret} = $client_secret; |
|
333
|
|
|
|
|
|
|
|
|
334
|
|
|
|
|
|
|
} |
|
335
|
|
|
|
|
|
|
else { |
|
336
|
|
|
|
|
|
|
|
|
337
|
0
|
|
|
|
|
|
die("token_endpoint $token_endpoint does not support client_secret_basic or client_secret_post"); |
|
338
|
|
|
|
|
|
|
|
|
339
|
|
|
|
|
|
|
} |
|
340
|
|
|
|
|
|
|
|
|
341
|
0
|
|
|
|
|
|
my $res = $self->ua->post( |
|
342
|
|
|
|
|
|
|
$token_endpoint, |
|
343
|
|
|
|
|
|
|
$params, |
|
344
|
|
|
|
|
|
|
%$headers |
|
345
|
|
|
|
|
|
|
); |
|
346
|
|
|
|
|
|
|
|
|
347
|
0
|
0
|
|
|
|
|
die("$token_endpoint returned invalid content type ".$res->content_type) |
|
348
|
|
|
|
|
|
|
unless $res->content_type =~ /json/o; |
|
349
|
|
|
|
|
|
|
|
|
350
|
0
|
|
|
|
|
|
$self->json->decode($res->content); |
|
351
|
|
|
|
|
|
|
} |
|
352
|
|
|
|
|
|
|
|
|
353
|
|
|
|
|
|
|
sub to_app { |
|
354
|
|
|
|
|
|
|
|
|
355
|
0
|
|
|
0
|
0
|
|
my $self = $_[0]; |
|
356
|
|
|
|
|
|
|
|
|
357
|
|
|
|
|
|
|
sub { |
|
358
|
|
|
|
|
|
|
|
|
359
|
0
|
|
|
0
|
|
|
my $env = $_[0]; |
|
360
|
0
|
|
|
|
|
|
my $log = $self->log(); |
|
361
|
|
|
|
|
|
|
|
|
362
|
0
|
|
|
|
|
|
my $request = Plack::Request->new($env); |
|
363
|
0
|
|
|
|
|
|
my $session = Plack::Session->new($env); |
|
364
|
0
|
|
|
|
|
|
my $query_params = $request->query_parameters(); |
|
365
|
|
|
|
|
|
|
|
|
366
|
0
|
0
|
|
|
|
|
if( $self->log->is_debug() ){ |
|
367
|
|
|
|
|
|
|
|
|
368
|
0
|
|
|
|
|
|
$self->log->debugf( "incoming query parameters: %s", [$query_params->flatten] ); |
|
369
|
0
|
|
|
|
|
|
$self->log->debugf( "session: %s", $session->dump() ); |
|
370
|
0
|
|
|
|
|
|
$self->log->debugf( "session_key for auth_sso: %s" . $self->session_key() ); |
|
371
|
|
|
|
|
|
|
|
|
372
|
|
|
|
|
|
|
} |
|
373
|
|
|
|
|
|
|
|
|
374
|
0
|
0
|
|
|
|
|
if ( $request->method ne "GET" ) { |
|
375
|
|
|
|
|
|
|
|
|
376
|
0
|
|
|
|
|
|
$self->log->errorf("invalid http method %s", $request->method); |
|
377
|
0
|
|
|
|
|
|
return [400, [ "Content-Type" => "text/plain" ], ["invalid http method"]]; |
|
378
|
|
|
|
|
|
|
|
|
379
|
|
|
|
|
|
|
} |
|
380
|
|
|
|
|
|
|
|
|
381
|
0
|
|
|
|
|
|
my $state = $query_params->get("state"); |
|
382
|
0
|
|
|
|
|
|
my $stored_state = $self->get_csrf_token($session); |
|
383
|
|
|
|
|
|
|
|
|
384
|
|
|
|
|
|
|
# remove auth_sso from possibly previous successfull authentication |
|
385
|
|
|
|
|
|
|
# (allowing for reauthentication) |
|
386
|
0
|
|
|
|
|
|
$session->remove($self->session_key); |
|
387
|
|
|
|
|
|
|
|
|
388
|
|
|
|
|
|
|
# redirect to authorization url |
|
389
|
0
|
0
|
0
|
|
|
|
if ( !(is_string($stored_state) && is_string($state)) ) { |
|
390
|
|
|
|
|
|
|
|
|
391
|
0
|
|
|
|
|
|
$self->cleanup($session); |
|
392
|
|
|
|
|
|
|
|
|
393
|
0
|
|
|
|
|
|
my $authorization_uri = $self->generate_authorization_uri( |
|
394
|
|
|
|
|
|
|
request => $request, |
|
395
|
|
|
|
|
|
|
session => $session |
|
396
|
|
|
|
|
|
|
); |
|
397
|
|
|
|
|
|
|
|
|
398
|
0
|
|
|
|
|
|
return [302, [Location => $authorization_uri], []]; |
|
399
|
|
|
|
|
|
|
|
|
400
|
|
|
|
|
|
|
} |
|
401
|
|
|
|
|
|
|
|
|
402
|
|
|
|
|
|
|
# check csrf |
|
403
|
0
|
0
|
|
|
|
|
if ( $stored_state ne $state ) { |
|
404
|
|
|
|
|
|
|
|
|
405
|
0
|
|
|
|
|
|
$self->cleanup($session); |
|
406
|
0
|
|
|
|
|
|
$self->set_auth_sso_error( $session,{ |
|
407
|
|
|
|
|
|
|
package => __PACKAGE__, |
|
408
|
|
|
|
|
|
|
package_id => $self->id, |
|
409
|
|
|
|
|
|
|
type => "CSRF_DETECTED", |
|
410
|
|
|
|
|
|
|
content => "CSRF_DETECTED" |
|
411
|
|
|
|
|
|
|
}); |
|
412
|
0
|
|
|
|
|
|
return $self->redirect_to_error(); |
|
413
|
|
|
|
|
|
|
|
|
414
|
|
|
|
|
|
|
} |
|
415
|
|
|
|
|
|
|
|
|
416
|
|
|
|
|
|
|
# validate authorization returned from idp |
|
417
|
0
|
|
|
|
|
|
my $error = $query_params->get("error"); |
|
418
|
0
|
|
|
|
|
|
my $error_description = $query_params->get("error_description"); |
|
419
|
|
|
|
|
|
|
|
|
420
|
0
|
0
|
|
|
|
|
if ( is_string($error) ) { |
|
421
|
|
|
|
|
|
|
|
|
422
|
0
|
|
|
|
|
|
$self->cleanup($session); |
|
423
|
0
|
|
|
|
|
|
$self->set_auth_sso_error($session, { |
|
424
|
|
|
|
|
|
|
package => __PACKAGE__, |
|
425
|
|
|
|
|
|
|
package_id => $self->id, |
|
426
|
|
|
|
|
|
|
type => $error, |
|
427
|
|
|
|
|
|
|
content => $error_description |
|
428
|
|
|
|
|
|
|
}); |
|
429
|
0
|
|
|
|
|
|
return $self->redirect_to_error(); |
|
430
|
|
|
|
|
|
|
|
|
431
|
|
|
|
|
|
|
} |
|
432
|
|
|
|
|
|
|
|
|
433
|
0
|
|
|
|
|
|
my $code = $query_params->get("code"); |
|
434
|
|
|
|
|
|
|
|
|
435
|
0
|
0
|
|
|
|
|
unless ( is_string($code) ) { |
|
436
|
|
|
|
|
|
|
|
|
437
|
0
|
|
|
|
|
|
$self->cleanup($session); |
|
438
|
0
|
|
|
|
|
|
$self->set_auth_sso_error($session, { |
|
439
|
|
|
|
|
|
|
package => __PACKAGE__, |
|
440
|
|
|
|
|
|
|
package_id => $self->id, |
|
441
|
|
|
|
|
|
|
type => "AUTH_SSO_OIDC_AUTHORIZATION_NO_CODE", |
|
442
|
|
|
|
|
|
|
content => "oidc authorization endpoint did not return query parameter code" |
|
443
|
|
|
|
|
|
|
}); |
|
444
|
0
|
|
|
|
|
|
return $self->redirect_to_error(); |
|
445
|
|
|
|
|
|
|
|
|
446
|
|
|
|
|
|
|
} |
|
447
|
|
|
|
|
|
|
|
|
448
|
0
|
|
|
|
|
|
my $tokens = $self->exchange_code_for_tokens( |
|
449
|
|
|
|
|
|
|
request => $request, |
|
450
|
|
|
|
|
|
|
session => $session, |
|
451
|
|
|
|
|
|
|
code => $code |
|
452
|
|
|
|
|
|
|
); |
|
453
|
|
|
|
|
|
|
|
|
454
|
0
|
0
|
|
|
|
|
$self->log->debugf("tokens: %s", $tokens) |
|
455
|
|
|
|
|
|
|
if $self->log->is_debug(); |
|
456
|
|
|
|
|
|
|
|
|
457
|
0
|
0
|
|
|
|
|
if ( is_string($tokens->{error}) ) { |
|
458
|
|
|
|
|
|
|
|
|
459
|
0
|
|
|
|
|
|
$self->cleanup($session); |
|
460
|
|
|
|
|
|
|
$self->set_auth_sso_error($session, { |
|
461
|
|
|
|
|
|
|
package => __PACKAGE__, |
|
462
|
|
|
|
|
|
|
package_id => $self->id, |
|
463
|
|
|
|
|
|
|
type => $tokens->{error}, |
|
464
|
|
|
|
|
|
|
content => $tokens->{error_description} |
|
465
|
0
|
|
|
|
|
|
}); |
|
466
|
0
|
|
|
|
|
|
return $self->redirect_to_error(); |
|
467
|
|
|
|
|
|
|
|
|
468
|
|
|
|
|
|
|
} |
|
469
|
|
|
|
|
|
|
|
|
470
|
0
|
|
|
|
|
|
my $claims = $self->extract_claims_from_id_token($tokens->{id_token}); |
|
471
|
|
|
|
|
|
|
|
|
472
|
0
|
0
|
|
|
|
|
$self->log->debugf("claims: %s", $claims) |
|
473
|
|
|
|
|
|
|
if $self->log->is_debug(); |
|
474
|
|
|
|
|
|
|
|
|
475
|
0
|
|
|
|
|
|
$self->cleanup($session); |
|
476
|
|
|
|
|
|
|
|
|
477
|
|
|
|
|
|
|
my $session_data = +{ |
|
478
|
|
|
|
|
|
|
extra => {}, |
|
479
|
|
|
|
|
|
|
info => $claims, |
|
480
|
0
|
|
|
|
|
|
uid => $claims->{ $self->uid_key() }, |
|
481
|
|
|
|
|
|
|
package => __PACKAGE__, |
|
482
|
|
|
|
|
|
|
package_id => $self->id, |
|
483
|
|
|
|
|
|
|
}; |
|
484
|
|
|
|
|
|
|
|
|
485
|
0
|
0
|
|
|
|
|
if ($self->store_oidc_response()) { |
|
486
|
|
|
|
|
|
|
$session_data->{response} = +{ |
|
487
|
0
|
|
|
|
|
|
content => $self->json->encode($tokens), |
|
488
|
|
|
|
|
|
|
content_type => "application/json" |
|
489
|
|
|
|
|
|
|
}; |
|
490
|
|
|
|
|
|
|
} |
|
491
|
|
|
|
|
|
|
|
|
492
|
0
|
|
|
|
|
|
$self->set_auth_sso($session, $session_data); |
|
493
|
|
|
|
|
|
|
|
|
494
|
0
|
0
|
|
|
|
|
$self->log->debugf("auth_sso: %s", $self->get_auth_sso($session)) |
|
495
|
|
|
|
|
|
|
if $self->log->is_debug(); |
|
496
|
|
|
|
|
|
|
|
|
497
|
0
|
|
|
|
|
|
return $self->redirect_to_authorization(); |
|
498
|
|
|
|
|
|
|
|
|
499
|
0
|
|
|
|
|
|
}; |
|
500
|
|
|
|
|
|
|
|
|
501
|
|
|
|
|
|
|
} |
|
502
|
|
|
|
|
|
|
|
|
503
|
|
|
|
|
|
|
1; |
|
504
|
|
|
|
|
|
|
|
|
505
|
|
|
|
|
|
|
=pod |
|
506
|
|
|
|
|
|
|
|
|
507
|
|
|
|
|
|
|
=head1 NAME |
|
508
|
|
|
|
|
|
|
|
|
509
|
|
|
|
|
|
|
Plack::Auth::SSO::OIDC - implementation of OpenID Connect for Plack::Auth::SSO |
|
510
|
|
|
|
|
|
|
|
|
511
|
|
|
|
|
|
|
=begin markdown |
|
512
|
|
|
|
|
|
|
|
|
513
|
|
|
|
|
|
|
# STATUS |
|
514
|
|
|
|
|
|
|
|
|
515
|
|
|
|
|
|
|
[](https://travis-ci.org/LibreCat/Plack-Auth-SSO-OIDC) |
|
516
|
|
|
|
|
|
|
[](https://coveralls.io/r/LibreCat/Plack-Auth-SSO-OIDC) |
|
517
|
|
|
|
|
|
|
[](http://cpants.cpanauthors.org/dist/Plack-Auth-SSO-OIDC) |
|
518
|
|
|
|
|
|
|
|
|
519
|
|
|
|
|
|
|
=end markdown |
|
520
|
|
|
|
|
|
|
|
|
521
|
|
|
|
|
|
|
=head1 DESCRIPTION |
|
522
|
|
|
|
|
|
|
|
|
523
|
|
|
|
|
|
|
This is an implementation of L to authenticate against a openid connect server. |
|
524
|
|
|
|
|
|
|
|
|
525
|
|
|
|
|
|
|
It inherits all configuration options from its parent. |
|
526
|
|
|
|
|
|
|
|
|
527
|
|
|
|
|
|
|
=head1 SYNOPSIS |
|
528
|
|
|
|
|
|
|
|
|
529
|
|
|
|
|
|
|
# in your app.psi (Plack) |
|
530
|
|
|
|
|
|
|
|
|
531
|
|
|
|
|
|
|
use strict; |
|
532
|
|
|
|
|
|
|
use warnings; |
|
533
|
|
|
|
|
|
|
use Plack::Builder; |
|
534
|
|
|
|
|
|
|
use JSON; |
|
535
|
|
|
|
|
|
|
use Plack::Auth::SSO::OIDC; |
|
536
|
|
|
|
|
|
|
use Plack::Session::Store::File; |
|
537
|
|
|
|
|
|
|
|
|
538
|
|
|
|
|
|
|
my $uri_base = "http://localhost:5000"; |
|
539
|
|
|
|
|
|
|
|
|
540
|
|
|
|
|
|
|
builder { |
|
541
|
|
|
|
|
|
|
|
|
542
|
|
|
|
|
|
|
# session middleware needed to store "auth_sso" and/or "auth_sso_error" |
|
543
|
|
|
|
|
|
|
# in memory session store for testing purposes |
|
544
|
|
|
|
|
|
|
enable "Session"; |
|
545
|
|
|
|
|
|
|
|
|
546
|
|
|
|
|
|
|
# for authentication, redirect your users to this path |
|
547
|
|
|
|
|
|
|
mount "/auth/oidc" => Plack::Auth::SSO::OIDC->new( |
|
548
|
|
|
|
|
|
|
|
|
549
|
|
|
|
|
|
|
# plack application needs to know about the base url of this application |
|
550
|
|
|
|
|
|
|
uri_base => $uri_base, |
|
551
|
|
|
|
|
|
|
|
|
552
|
|
|
|
|
|
|
# after successfull authentication, user is redirected to this path (uri_base is used!) |
|
553
|
|
|
|
|
|
|
authorization_path => "/auth/callback", |
|
554
|
|
|
|
|
|
|
|
|
555
|
|
|
|
|
|
|
# when authentication fails at the identity provider |
|
556
|
|
|
|
|
|
|
# user is redirected to this path with session key "auth_sso_error" (hash) |
|
557
|
|
|
|
|
|
|
error_path => "/auth/error", |
|
558
|
|
|
|
|
|
|
|
|
559
|
|
|
|
|
|
|
# openid connect discovery url |
|
560
|
|
|
|
|
|
|
openid_uri => "https://example.oidc.org/auth/oidc/.well-known/openid-configuration", |
|
561
|
|
|
|
|
|
|
client_id => "my-client-id", |
|
562
|
|
|
|
|
|
|
client_secret => "myclient-secret", |
|
563
|
|
|
|
|
|
|
uid_key => "email" |
|
564
|
|
|
|
|
|
|
|
|
565
|
|
|
|
|
|
|
)->to_app(); |
|
566
|
|
|
|
|
|
|
|
|
567
|
|
|
|
|
|
|
# example psgi app that is called after successfull authentication at /auth/oidc (see above) |
|
568
|
|
|
|
|
|
|
# it expects session key "auth_sso" to be present |
|
569
|
|
|
|
|
|
|
# here you typically create a user session based on the uid in "auth_sso" |
|
570
|
|
|
|
|
|
|
mount "/auth/callback" => sub { |
|
571
|
|
|
|
|
|
|
|
|
572
|
|
|
|
|
|
|
my $env = shift; |
|
573
|
|
|
|
|
|
|
my $session = Plack::Session->new($env); |
|
574
|
|
|
|
|
|
|
my $auth_sso= $session->get("auth_sso"); |
|
575
|
|
|
|
|
|
|
my $user = MyUsers->get( $auth_sso->{uid} ); |
|
576
|
|
|
|
|
|
|
$session->set("user_id", $user->{id}); |
|
577
|
|
|
|
|
|
|
[ 200, [ "Content-Type" => "text/plain" ], [ |
|
578
|
|
|
|
|
|
|
"logged in! ", $user->{name} |
|
579
|
|
|
|
|
|
|
]]; |
|
580
|
|
|
|
|
|
|
|
|
581
|
|
|
|
|
|
|
}; |
|
582
|
|
|
|
|
|
|
|
|
583
|
|
|
|
|
|
|
# example psgi app that is called after unsuccessfull authentication at /auth/oidc (see above) |
|
584
|
|
|
|
|
|
|
# it expects session key "auth_sso_error" to be present |
|
585
|
|
|
|
|
|
|
mount "/auth/error" => sub { |
|
586
|
|
|
|
|
|
|
|
|
587
|
|
|
|
|
|
|
my $env = shift; |
|
588
|
|
|
|
|
|
|
my $session = Plack::Session->new($env); |
|
589
|
|
|
|
|
|
|
my $auth_sso_error = $session->get("auth_sso_error"); |
|
590
|
|
|
|
|
|
|
|
|
591
|
|
|
|
|
|
|
[ 200, [ "Content-Type" => "text/plain" ], [ |
|
592
|
|
|
|
|
|
|
"something happened during single sign on authentication: ", |
|
593
|
|
|
|
|
|
|
$auth_sso_error->{content} |
|
594
|
|
|
|
|
|
|
]]; |
|
595
|
|
|
|
|
|
|
|
|
596
|
|
|
|
|
|
|
}; |
|
597
|
|
|
|
|
|
|
}; |
|
598
|
|
|
|
|
|
|
|
|
599
|
|
|
|
|
|
|
=head1 CONSTRUCTOR ARGUMENTS |
|
600
|
|
|
|
|
|
|
|
|
601
|
|
|
|
|
|
|
=over 4 |
|
602
|
|
|
|
|
|
|
|
|
603
|
|
|
|
|
|
|
=item C<< uri_base >> |
|
604
|
|
|
|
|
|
|
|
|
605
|
|
|
|
|
|
|
See L |
|
606
|
|
|
|
|
|
|
|
|
607
|
|
|
|
|
|
|
=item C<< id >> |
|
608
|
|
|
|
|
|
|
|
|
609
|
|
|
|
|
|
|
See L |
|
610
|
|
|
|
|
|
|
|
|
611
|
|
|
|
|
|
|
=item C<< session_key >> |
|
612
|
|
|
|
|
|
|
|
|
613
|
|
|
|
|
|
|
See L |
|
614
|
|
|
|
|
|
|
|
|
615
|
|
|
|
|
|
|
=item C<< authorization_path >> |
|
616
|
|
|
|
|
|
|
|
|
617
|
|
|
|
|
|
|
See L |
|
618
|
|
|
|
|
|
|
|
|
619
|
|
|
|
|
|
|
=item C<< error_path >> |
|
620
|
|
|
|
|
|
|
|
|
621
|
|
|
|
|
|
|
See L |
|
622
|
|
|
|
|
|
|
|
|
623
|
|
|
|
|
|
|
=item C<< openid_uri >> |
|
624
|
|
|
|
|
|
|
|
|
625
|
|
|
|
|
|
|
base url of the OIDC discovery url. |
|
626
|
|
|
|
|
|
|
|
|
627
|
|
|
|
|
|
|
typically an url that ends on C<< /.well-known/openid-configuration >> |
|
628
|
|
|
|
|
|
|
|
|
629
|
|
|
|
|
|
|
=item C<< client_id >> |
|
630
|
|
|
|
|
|
|
|
|
631
|
|
|
|
|
|
|
client-id as given by the OIDC service |
|
632
|
|
|
|
|
|
|
|
|
633
|
|
|
|
|
|
|
=item C<< client_secret >> |
|
634
|
|
|
|
|
|
|
|
|
635
|
|
|
|
|
|
|
client-secret as given by the OIDC service |
|
636
|
|
|
|
|
|
|
|
|
637
|
|
|
|
|
|
|
=item C<< scope >> |
|
638
|
|
|
|
|
|
|
|
|
639
|
|
|
|
|
|
|
Scope requested from the OIDC service. |
|
640
|
|
|
|
|
|
|
|
|
641
|
|
|
|
|
|
|
Space separated string containing all scopes |
|
642
|
|
|
|
|
|
|
|
|
643
|
|
|
|
|
|
|
Default: C<< "openid profile email" >> |
|
644
|
|
|
|
|
|
|
|
|
645
|
|
|
|
|
|
|
Please include scope C<< "openid" >> |
|
646
|
|
|
|
|
|
|
|
|
647
|
|
|
|
|
|
|
cf. L |
|
648
|
|
|
|
|
|
|
|
|
649
|
|
|
|
|
|
|
=item C<< authorize_params >> |
|
650
|
|
|
|
|
|
|
|
|
651
|
|
|
|
|
|
|
Hash reference of parameters (values must be strings) that are added to |
|
652
|
|
|
|
|
|
|
|
|
653
|
|
|
|
|
|
|
the authorization url. Empty by default |
|
654
|
|
|
|
|
|
|
|
|
655
|
|
|
|
|
|
|
e.g. C<< { prompt => "login", "kc_idp_hint" => "orcid" } >> |
|
656
|
|
|
|
|
|
|
|
|
657
|
|
|
|
|
|
|
Note that some parameters are set internally |
|
658
|
|
|
|
|
|
|
|
|
659
|
|
|
|
|
|
|
and therefore will have no effect: |
|
660
|
|
|
|
|
|
|
|
|
661
|
|
|
|
|
|
|
=over 6 |
|
662
|
|
|
|
|
|
|
|
|
663
|
|
|
|
|
|
|
=item C<< code_challenge >> |
|
664
|
|
|
|
|
|
|
|
|
665
|
|
|
|
|
|
|
=item C<< code_challenge_method >> |
|
666
|
|
|
|
|
|
|
|
|
667
|
|
|
|
|
|
|
=item C<< state >> |
|
668
|
|
|
|
|
|
|
|
|
669
|
|
|
|
|
|
|
=item C<< scope >> |
|
670
|
|
|
|
|
|
|
|
|
671
|
|
|
|
|
|
|
=item C<< client_id >> |
|
672
|
|
|
|
|
|
|
|
|
673
|
|
|
|
|
|
|
=item C<< response_type >> |
|
674
|
|
|
|
|
|
|
|
|
675
|
|
|
|
|
|
|
=item C<< redirect_uri >> |
|
676
|
|
|
|
|
|
|
|
|
677
|
|
|
|
|
|
|
=back |
|
678
|
|
|
|
|
|
|
|
|
679
|
|
|
|
|
|
|
=item C<< allowed_authorize_params >> |
|
680
|
|
|
|
|
|
|
|
|
681
|
|
|
|
|
|
|
Array reference of parameter names. |
|
682
|
|
|
|
|
|
|
|
|
683
|
|
|
|
|
|
|
When constructing the authorization url, |
|
684
|
|
|
|
|
|
|
|
|
685
|
|
|
|
|
|
|
these parameters are copied from the current url query |
|
686
|
|
|
|
|
|
|
|
|
687
|
|
|
|
|
|
|
to the authorization url. This allows to add some |
|
688
|
|
|
|
|
|
|
|
|
689
|
|
|
|
|
|
|
dynamic configuration, but should be used with caution. |
|
690
|
|
|
|
|
|
|
|
|
691
|
|
|
|
|
|
|
Note that parameters from C<< authorize_params >> always |
|
692
|
|
|
|
|
|
|
|
|
693
|
|
|
|
|
|
|
take precedence. |
|
694
|
|
|
|
|
|
|
|
|
695
|
|
|
|
|
|
|
=item C<< uid_key >> |
|
696
|
|
|
|
|
|
|
|
|
697
|
|
|
|
|
|
|
Attribute from claims to be used as uid |
|
698
|
|
|
|
|
|
|
|
|
699
|
|
|
|
|
|
|
Note that all claims are also stored in C<< $session->get("auth_sso")->{info} >> |
|
700
|
|
|
|
|
|
|
|
|
701
|
|
|
|
|
|
|
=item C<< store_oidc_response >> |
|
702
|
|
|
|
|
|
|
|
|
703
|
|
|
|
|
|
|
Store C<< content >> and C<< content_type >> of returned OIDC response in session key `auth_sso.response.content` and `auth_sso.response.content_type` |
|
704
|
|
|
|
|
|
|
respectively. This can exhaust the cookie length if all session data is stored in the cookie. |
|
705
|
|
|
|
|
|
|
|
|
706
|
|
|
|
|
|
|
Default: C<< 1 >> |
|
707
|
|
|
|
|
|
|
|
|
708
|
|
|
|
|
|
|
=back |
|
709
|
|
|
|
|
|
|
|
|
710
|
|
|
|
|
|
|
=head1 HOW IT WORKS |
|
711
|
|
|
|
|
|
|
|
|
712
|
|
|
|
|
|
|
=over 4 |
|
713
|
|
|
|
|
|
|
|
|
714
|
|
|
|
|
|
|
=item the openid configuration is retrieved from C<< {openid_uri} >> |
|
715
|
|
|
|
|
|
|
|
|
716
|
|
|
|
|
|
|
=over 6 |
|
717
|
|
|
|
|
|
|
|
|
718
|
|
|
|
|
|
|
=item key C<< authorization_endpoint >> must be present in openid configuration |
|
719
|
|
|
|
|
|
|
|
|
720
|
|
|
|
|
|
|
=item key C<< token_endpoint >> must be present in openid configuration |
|
721
|
|
|
|
|
|
|
|
|
722
|
|
|
|
|
|
|
=item key C<< jwks_uri >> must be present in openid configuration |
|
723
|
|
|
|
|
|
|
|
|
724
|
|
|
|
|
|
|
=item the user is redirected to the authorization endpoint with extra query parameters |
|
725
|
|
|
|
|
|
|
|
|
726
|
|
|
|
|
|
|
=back |
|
727
|
|
|
|
|
|
|
|
|
728
|
|
|
|
|
|
|
=item after authentication at the authorization endpoint, the user is redirected back to this url with query parameters C<< code >> and C<< state >>. When something happened at the authorization endpoint, query parameters C<< error >> and C<< error_description >> are returned, and no C<< code >>. |
|
729
|
|
|
|
|
|
|
|
|
730
|
|
|
|
|
|
|
=item C<< code >> is exchanged for a json string, using the token endpoint. This json string is a record that contains attributes like C<< id_token >> and C<< access_token >>. See L for more information. |
|
731
|
|
|
|
|
|
|
|
|
732
|
|
|
|
|
|
|
=item key C<< id_token >> in the token json string contains three parts: |
|
733
|
|
|
|
|
|
|
|
|
734
|
|
|
|
|
|
|
=over 6 |
|
735
|
|
|
|
|
|
|
|
|
736
|
|
|
|
|
|
|
=item jwt jose header. Can be decoded with base64 into a json string |
|
737
|
|
|
|
|
|
|
|
|
738
|
|
|
|
|
|
|
=item jwt payload. Can be decoded with base64 into a json string |
|
739
|
|
|
|
|
|
|
|
|
740
|
|
|
|
|
|
|
=item jwt signature |
|
741
|
|
|
|
|
|
|
|
|
742
|
|
|
|
|
|
|
=back |
|
743
|
|
|
|
|
|
|
|
|
744
|
|
|
|
|
|
|
=item the jwt payload from the C<< id_token >> is decoded into a json string and then to a perl hash. All this data is stored C<< $session->{auth_sso}->{info} >>. One of these attributes will be the uid that will be stored at C<< $session->{auth_sso}->{uid} >>. This is determined by configuration key C<< uid_key >> (see above). e.g. "email" |
|
745
|
|
|
|
|
|
|
|
|
746
|
|
|
|
|
|
|
=back |
|
747
|
|
|
|
|
|
|
|
|
748
|
|
|
|
|
|
|
=head1 NOTES |
|
749
|
|
|
|
|
|
|
|
|
750
|
|
|
|
|
|
|
=over 4 |
|
751
|
|
|
|
|
|
|
|
|
752
|
|
|
|
|
|
|
=item Can I reauthenticate when I visit the application? |
|
753
|
|
|
|
|
|
|
|
|
754
|
|
|
|
|
|
|
When this Plack application is for example mounted at |
|
755
|
|
|
|
|
|
|
|
|
756
|
|
|
|
|
|
|
C<< /auth/oidc >>, then you can reauthenticate by visiting |
|
757
|
|
|
|
|
|
|
|
|
758
|
|
|
|
|
|
|
it again, but it depends on your configuration what actually |
|
759
|
|
|
|
|
|
|
|
|
760
|
|
|
|
|
|
|
happens at the openid connect server. If C<< prompt >> is not |
|
761
|
|
|
|
|
|
|
|
|
762
|
|
|
|
|
|
|
set anywhere (neither in C<< authorize_params >> nor in the |
|
763
|
|
|
|
|
|
|
|
|
764
|
|
|
|
|
|
|
current url if that is allowed), then the external server |
|
765
|
|
|
|
|
|
|
|
|
766
|
|
|
|
|
|
|
will just sent you back with the same tokens. |
|
767
|
|
|
|
|
|
|
|
|
768
|
|
|
|
|
|
|
Note that C<< session("auth_sso") >> is removed at the start |
|
769
|
|
|
|
|
|
|
|
|
770
|
|
|
|
|
|
|
of every (re)authentication. |
|
771
|
|
|
|
|
|
|
|
|
772
|
|
|
|
|
|
|
=back |
|
773
|
|
|
|
|
|
|
|
|
774
|
|
|
|
|
|
|
=head1 LOGGING |
|
775
|
|
|
|
|
|
|
|
|
776
|
|
|
|
|
|
|
All subclasses of L use L |
|
777
|
|
|
|
|
|
|
to log messages to the category that equals the current |
|
778
|
|
|
|
|
|
|
package name. |
|
779
|
|
|
|
|
|
|
|
|
780
|
|
|
|
|
|
|
=head1 AUTHOR |
|
781
|
|
|
|
|
|
|
|
|
782
|
|
|
|
|
|
|
Nicolas Franck, C<< >> |
|
783
|
|
|
|
|
|
|
|
|
784
|
|
|
|
|
|
|
=head1 LICENSE AND COPYRIGHT |
|
785
|
|
|
|
|
|
|
|
|
786
|
|
|
|
|
|
|
This program is free software; you can redistribute it and/or modify it |
|
787
|
|
|
|
|
|
|
under the terms of either: the GNU General Public License as published |
|
788
|
|
|
|
|
|
|
by the Free Software Foundation; or the Artistic License. |
|
789
|
|
|
|
|
|
|
|
|
790
|
|
|
|
|
|
|
See L for more information. |
|
791
|
|
|
|
|
|
|
|
|
792
|
|
|
|
|
|
|
=head1 SEE ALSO |
|
793
|
|
|
|
|
|
|
|
|
794
|
|
|
|
|
|
|
L |
|
795
|
|
|
|
|
|
|
|
|
796
|
|
|
|
|
|
|
=cut |