line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package LWP::UserAgent::msgraph;
|
2
|
|
|
|
|
|
|
|
3
|
1
|
|
|
1
|
|
67730
|
use strict;
|
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
29
|
|
4
|
1
|
|
|
1
|
|
14
|
use warnings;
|
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
37
|
|
5
|
|
|
|
|
|
|
|
6
|
|
|
|
|
|
|
our $VERSION = '0.04';
|
7
|
|
|
|
|
|
|
|
8
|
1
|
|
|
1
|
|
483
|
use parent 'LWP::UserAgent';
|
|
1
|
|
|
|
|
315
|
|
|
1
|
|
|
|
|
5
|
|
9
|
|
|
|
|
|
|
|
10
|
1
|
|
|
1
|
|
54000
|
use JSON;
|
|
1
|
|
|
|
|
10252
|
|
|
1
|
|
|
|
|
5
|
|
11
|
1
|
|
|
1
|
|
785
|
use Storable;
|
|
1
|
|
|
|
|
2829
|
|
|
1
|
|
|
|
|
61
|
|
12
|
1
|
|
|
1
|
|
467
|
use Data::UUID;
|
|
1
|
|
|
|
|
614
|
|
|
1
|
|
|
|
|
72
|
|
13
|
1
|
|
|
1
|
|
7
|
use File::Spec;
|
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
24
|
|
14
|
1
|
|
|
1
|
|
5
|
use Storable;
|
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
36
|
|
15
|
1
|
|
|
1
|
|
11
|
use Carp;
|
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
52
|
|
16
|
1
|
|
|
1
|
|
9
|
use URI;
|
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
24
|
|
17
|
1
|
|
|
1
|
|
479
|
use HTTP::Request::Common;
|
|
1
|
|
|
|
|
2268
|
|
|
1
|
|
|
|
|
69
|
|
18
|
1
|
|
|
1
|
|
494
|
use Net::EmptyPort qw(listen_socket empty_port check_port);
|
|
1
|
|
|
|
|
40476
|
|
|
1
|
|
|
|
|
1715
|
|
19
|
|
|
|
|
|
|
|
20
|
|
|
|
|
|
|
sub new($%) {
|
21
|
|
|
|
|
|
|
|
22
|
1
|
|
|
1
|
1
|
147
|
my %internals;
|
23
|
|
|
|
|
|
|
|
24
|
1
|
|
|
|
|
3
|
my $class=shift();
|
25
|
|
|
|
|
|
|
|
26
|
1
|
|
|
|
|
5
|
my %args=@_;
|
27
|
|
|
|
|
|
|
|
28
|
|
|
|
|
|
|
#This are our lwp-extended options
|
29
|
1
|
|
|
|
|
5
|
for (qw(appid secret grant_type scope persistent sid base store return_url tenant local_port)) {
|
30
|
11
|
100
|
|
|
|
28
|
if (exists $args{$_}) {
|
31
|
3
|
|
|
|
|
6
|
$internals{$_}= $args{$_};
|
32
|
3
|
|
|
|
|
6
|
delete $args{$_};
|
33
|
|
|
|
|
|
|
}
|
34
|
|
|
|
|
|
|
}
|
35
|
|
|
|
|
|
|
|
36
|
|
|
|
|
|
|
#Some defaults
|
37
|
1
|
50
|
|
|
|
4
|
unless (exists $internals{sid}) {
|
38
|
1
|
|
|
|
|
470
|
my $guid=Data::UUID->new;
|
39
|
1
|
|
|
|
|
318
|
$internals{sid}=$guid->create_str();
|
40
|
|
|
|
|
|
|
}
|
41
|
|
|
|
|
|
|
|
42
|
1
|
|
|
|
|
8
|
my $sid=$internals{sid};
|
43
|
|
|
|
|
|
|
|
44
|
1
|
50
|
|
|
|
6
|
$internals{base}='https://graph.microsoft.com/v1.0' unless(exists $internals{base});
|
45
|
1
|
|
|
|
|
4
|
$internals{base} =~ s/\/$//;
|
46
|
|
|
|
|
|
|
|
47
|
1
|
50
|
|
|
|
5
|
$internals{console}=0 unless (exists $internals{console});
|
48
|
|
|
|
|
|
|
|
49
|
1
|
|
|
|
|
2
|
$internals{expires}=0;
|
50
|
1
|
50
|
|
|
|
5
|
$internals{local_port}=8081 unless ($internals{local_port});
|
51
|
|
|
|
|
|
|
|
52
|
|
|
|
|
|
|
#complain about missing options
|
53
|
1
|
|
|
|
|
3
|
for (qw(appid grant_type tenant)) {
|
54
|
3
|
50
|
|
|
|
9
|
croak "Missing mandatory option $_" unless (exists $internals{$_});
|
55
|
|
|
|
|
|
|
}
|
56
|
|
|
|
|
|
|
|
57
|
|
|
|
|
|
|
#Now the persistent thing
|
58
|
1
|
50
|
33
|
|
|
5
|
$internals{persistent}=1 if (exists $internals{store} && ! exists $internals{persistent});
|
59
|
1
|
50
|
|
|
|
5
|
$internals{persistent}=0 unless (exists $internals{persistent});
|
60
|
|
|
|
|
|
|
|
61
|
1
|
50
|
33
|
|
|
3
|
if ($internals{persistent} && ! exists $internals{store}) {
|
62
|
0
|
|
|
|
|
0
|
my $tmpdir = File::Spec->tmpdir();
|
63
|
0
|
|
|
|
|
0
|
$internals{store}="$tmpdir/$sid.tmp";
|
64
|
|
|
|
|
|
|
}
|
65
|
|
|
|
|
|
|
|
66
|
1
|
50
|
33
|
|
|
4
|
if ($internals{persistent} && -r $internals{store}) {
|
67
|
0
|
|
|
|
|
0
|
my $stored=retrieve($internals{store});
|
68
|
0
|
0
|
|
|
|
0
|
croak 'Mismatch persistent session' unless ($stored->{sid} eq $sid);
|
69
|
0
|
|
|
|
|
0
|
for (keys %$stored) {
|
70
|
0
|
|
|
|
|
0
|
$internals{$_}=$stored->{$_};
|
71
|
|
|
|
|
|
|
}
|
72
|
|
|
|
|
|
|
}
|
73
|
|
|
|
|
|
|
|
74
|
1
|
|
|
|
|
12
|
my $self=$class->SUPER::new(%args);
|
75
|
1
|
|
|
|
|
3014
|
for (keys %internals) {
|
76
|
9
|
|
|
|
|
19
|
$self->{$_} = $internals{$_};
|
77
|
|
|
|
|
|
|
}
|
78
|
|
|
|
|
|
|
|
79
|
1
|
|
|
|
|
5
|
return $self;
|
80
|
|
|
|
|
|
|
|
81
|
|
|
|
|
|
|
}
|
82
|
|
|
|
|
|
|
|
83
|
|
|
|
|
|
|
sub writestore($) {
|
84
|
|
|
|
|
|
|
|
85
|
0
|
|
|
0
|
0
|
|
my $self=shift();
|
86
|
|
|
|
|
|
|
|
87
|
0
|
0
|
|
|
|
|
croak 'Wrong writestore call on non-persistant client' unless ($self->{persistent});
|
88
|
|
|
|
|
|
|
|
89
|
0
|
|
|
|
|
|
my $data={};
|
90
|
|
|
|
|
|
|
|
91
|
|
|
|
|
|
|
#This is a subset of the runtime data. It's important that the secret is out
|
92
|
0
|
|
|
|
|
|
for (qw(access_token expires expires_in refresh_token token_type scope appid sid redirect_uri console)) {
|
93
|
0
|
|
|
|
|
|
$data->{$_}=$self->{$_};
|
94
|
|
|
|
|
|
|
}
|
95
|
0
|
|
|
|
|
|
return store $data, $self->{store};
|
96
|
|
|
|
|
|
|
}
|
97
|
|
|
|
|
|
|
|
98
|
|
|
|
|
|
|
sub request {
|
99
|
|
|
|
|
|
|
|
100
|
0
|
|
|
0
|
1
|
|
my ($self,$method, $url, $payload)=@_;
|
101
|
|
|
|
|
|
|
|
102
|
0
|
|
|
|
|
|
$url =~ s/^\///;
|
103
|
|
|
|
|
|
|
|
104
|
0
|
|
|
|
|
|
my $abs_uri=URI->new_abs($url, $self->{base}.'/');
|
105
|
|
|
|
|
|
|
|
106
|
0
|
|
|
|
|
|
my $req=HTTP::Request->new($method,"$abs_uri");
|
107
|
0
|
|
|
|
|
|
$req->header('Content-Type' => 'application/json');
|
108
|
0
|
|
|
|
|
|
$req->header('Accept' => 'application/json');
|
109
|
0
|
0
|
|
|
|
|
$req->content(to_json($payload)) if ($payload);
|
110
|
|
|
|
|
|
|
|
111
|
0
|
|
|
|
|
|
my $res=LWP::UserAgent::request($self,$req);
|
112
|
|
|
|
|
|
|
|
113
|
|
|
|
|
|
|
#Response code is a keeper
|
114
|
0
|
|
|
|
|
|
$self->{code}=$res->code;
|
115
|
|
|
|
|
|
|
|
116
|
0
|
0
|
|
|
|
|
if ($res->is_success) {
|
117
|
0
|
|
|
|
|
|
my $data=from_json($res->decoded_content);
|
118
|
0
|
0
|
|
|
|
|
if (exists $data->{'@odata.nextLink'}) {
|
119
|
0
|
|
|
|
|
|
$self->{nextLink}=$data->{'@odata.nextLink'};
|
120
|
|
|
|
|
|
|
} else {
|
121
|
0
|
|
|
|
|
|
$self->{nextLink}=0;
|
122
|
|
|
|
|
|
|
}
|
123
|
0
|
|
|
|
|
|
return $data;
|
124
|
|
|
|
|
|
|
} else {
|
125
|
0
|
|
|
|
|
|
croak $res->decoded_content
|
126
|
|
|
|
|
|
|
}
|
127
|
|
|
|
|
|
|
}
|
128
|
|
|
|
|
|
|
|
129
|
|
|
|
|
|
|
sub code($) {
|
130
|
|
|
|
|
|
|
|
131
|
0
|
|
|
0
|
0
|
|
my $self=shift();
|
132
|
0
|
|
|
|
|
|
return $self->{code};
|
133
|
|
|
|
|
|
|
}
|
134
|
|
|
|
|
|
|
|
135
|
|
|
|
|
|
|
sub next($) {
|
136
|
|
|
|
|
|
|
|
137
|
0
|
|
|
0
|
0
|
|
my $self=shift();
|
138
|
|
|
|
|
|
|
|
139
|
0
|
0
|
|
|
|
|
if ($self->{nextLink}) {
|
140
|
0
|
|
|
|
|
|
return $self->request('GET' => $self->{nextLink});
|
141
|
|
|
|
|
|
|
} else {
|
142
|
0
|
|
|
|
|
|
return 0;
|
143
|
|
|
|
|
|
|
}
|
144
|
|
|
|
|
|
|
}
|
145
|
|
|
|
|
|
|
|
146
|
|
|
|
|
|
|
sub authendpoint($) {
|
147
|
|
|
|
|
|
|
|
148
|
0
|
|
|
0
|
0
|
|
my $self=shift();
|
149
|
|
|
|
|
|
|
|
150
|
|
|
|
|
|
|
#This is an ugly url. Must be used as a GET or a redirect location, so can't be done as POST
|
151
|
0
|
|
|
|
|
|
my $url=URI->new("https://login.microsoftonline.com/".$self->{tenant}."/oauth2/v2.0/authorize");
|
152
|
|
|
|
|
|
|
|
153
|
|
|
|
|
|
|
#query_param_append comes handy, but was introduced in URI 5.16
|
154
|
0
|
|
|
|
|
|
$url->query_param_append('client_id' => $self->{appid});
|
155
|
0
|
|
|
|
|
|
$url->query_param_append('response_type' => 'code');
|
156
|
0
|
|
|
|
|
|
$url->query_param_append('redirect_uri' => $self->{redirect_uri});
|
157
|
0
|
|
|
|
|
|
$url->query_param_append('response_mode' => 'query');
|
158
|
0
|
|
|
|
|
|
$url->query_param_append('scope' => $self->{scope});
|
159
|
0
|
|
|
|
|
|
$url->query_param_append('state' => $self->{sid});
|
160
|
0
|
|
|
|
|
|
return "$url";
|
161
|
|
|
|
|
|
|
}
|
162
|
|
|
|
|
|
|
|
163
|
|
|
|
|
|
|
sub tokenendpoint($) {
|
164
|
|
|
|
|
|
|
|
165
|
0
|
|
|
0
|
0
|
|
my $self=shift();
|
166
|
0
|
|
|
|
|
|
return "https://login.microsoftonline.com/".$self->{tenant}."/oauth2/v2.0/token";
|
167
|
|
|
|
|
|
|
}
|
168
|
|
|
|
|
|
|
|
169
|
|
|
|
|
|
|
sub sid($) {
|
170
|
0
|
|
|
0
|
0
|
|
my $self=shift();
|
171
|
0
|
|
|
|
|
|
return $self->{sid};
|
172
|
|
|
|
|
|
|
}
|
173
|
|
|
|
|
|
|
|
174
|
|
|
|
|
|
|
sub consolecode($) {
|
175
|
|
|
|
|
|
|
|
176
|
0
|
|
|
0
|
0
|
|
my $self=shift();
|
177
|
|
|
|
|
|
|
|
178
|
0
|
|
|
|
|
|
my $port=$self->{local_port};
|
179
|
0
|
|
|
|
|
|
my $web=LWP::UserAgent::msgraph::srvauth->new($port);
|
180
|
|
|
|
|
|
|
|
181
|
|
|
|
|
|
|
#Even if it's local, this redirect_uri must be Azure-registered
|
182
|
0
|
|
|
|
|
|
$self->{redirect_uri}="http://localhost:$port/auth";
|
183
|
|
|
|
|
|
|
|
184
|
|
|
|
|
|
|
#In order to setup a well-behaved http mini-server, we launch the server as a separate background
|
185
|
|
|
|
|
|
|
#process using the HTTP::Server::Simple module.
|
186
|
|
|
|
|
|
|
#Since this will be a separate process, and we need the authorization code value, we setup a
|
187
|
|
|
|
|
|
|
#private listening socket so the child process can upload the code to us
|
188
|
0
|
|
|
|
|
|
my $socket=listen_socket();
|
189
|
0
|
|
|
|
|
|
$web->setcaller($self, $socket->sockport);
|
190
|
0
|
|
|
|
|
|
my $pid=$web->background();
|
191
|
|
|
|
|
|
|
|
192
|
0
|
|
|
|
|
|
my $client=$socket->accept();
|
193
|
0
|
|
|
|
|
|
my $data="";
|
194
|
0
|
|
|
|
|
|
$client->recv($data,1024);
|
195
|
|
|
|
|
|
|
|
196
|
0
|
|
|
|
|
|
my ($id,$code)=split /\s/, $data;
|
197
|
|
|
|
|
|
|
|
198
|
|
|
|
|
|
|
#Our session id is sent as the optional 'state' parameter
|
199
|
|
|
|
|
|
|
#This value comes back to us along with the authorization code
|
200
|
|
|
|
|
|
|
#Here, we honour the state value validation. If the state value
|
201
|
|
|
|
|
|
|
#is not a match, the authorization code is discarded
|
202
|
0
|
0
|
0
|
|
|
|
if ($id && $id eq $self->sid) {
|
203
|
0
|
|
|
|
|
|
print "Authorization code received. You can close the browser now\n";
|
204
|
0
|
|
|
|
|
|
return $code;
|
205
|
|
|
|
|
|
|
} else {
|
206
|
0
|
|
|
|
|
|
return 0;
|
207
|
|
|
|
|
|
|
}
|
208
|
|
|
|
|
|
|
}
|
209
|
|
|
|
|
|
|
|
210
|
|
|
|
|
|
|
sub auth {
|
211
|
|
|
|
|
|
|
|
212
|
0
|
|
|
0
|
0
|
|
my $self=shift();
|
213
|
|
|
|
|
|
|
|
214
|
0
|
|
|
|
|
|
my $post;
|
215
|
|
|
|
|
|
|
|
216
|
|
|
|
|
|
|
#Here comes the authentication handshake with the MS Graph platform
|
217
|
|
|
|
|
|
|
#This is all spoken in application/x-www-form-urlencoded, so we use
|
218
|
|
|
|
|
|
|
#the standard simple_request and HTTP::Request approach
|
219
|
|
|
|
|
|
|
|
220
|
|
|
|
|
|
|
#Client-credentials for user-less anonymous connection
|
221
|
0
|
0
|
|
|
|
|
if ($self->{grant_type} eq 'client_credentials') {
|
|
|
0
|
|
|
|
|
|
222
|
|
|
|
|
|
|
|
223
|
|
|
|
|
|
|
$post=HTTP::Request::Common::POST($self->tokenendpoint(),
|
224
|
|
|
|
|
|
|
[client_id => $self->{appid},
|
225
|
|
|
|
|
|
|
scope => 'https://graph.microsoft.com/.default',
|
226
|
|
|
|
|
|
|
client_secret=> $self->{secret},
|
227
|
|
|
|
|
|
|
grant_type => $self->{grant_type}
|
228
|
0
|
|
|
|
|
|
]);
|
229
|
|
|
|
|
|
|
|
230
|
|
|
|
|
|
|
#Delegated authorization for user-oriented interaction
|
231
|
|
|
|
|
|
|
} elsif ($self->{grant_type} eq 'authorization_code') {
|
232
|
|
|
|
|
|
|
|
233
|
0
|
|
|
|
|
|
my $code=shift();
|
234
|
0
|
0
|
0
|
|
|
|
$code=$self->consolecode() unless ($code || ! $self->{console});
|
235
|
0
|
0
|
|
|
|
|
croak 'Missing or invalid authorization code' unless ($code);
|
236
|
|
|
|
|
|
|
|
237
|
|
|
|
|
|
|
$post=HTTP::Request::Common::POST($self->tokenendpoint(),
|
238
|
|
|
|
|
|
|
[client_id => $self->{appid},
|
239
|
|
|
|
|
|
|
scope => $self->{scope},
|
240
|
|
|
|
|
|
|
code => $code,
|
241
|
|
|
|
|
|
|
redirect_uri => $self->{redirect_uri},
|
242
|
|
|
|
|
|
|
client_secret=> $self->{secret},
|
243
|
|
|
|
|
|
|
grant_type => $self->{grant_type}
|
244
|
0
|
|
|
|
|
|
]);
|
245
|
|
|
|
|
|
|
|
246
|
|
|
|
|
|
|
} else {
|
247
|
0
|
|
|
|
|
|
croak 'Missing or unsupported grant_type';
|
248
|
|
|
|
|
|
|
}
|
249
|
|
|
|
|
|
|
|
250
|
0
|
0
|
|
|
|
|
croak 'Authentication scheme error' unless ($post);
|
251
|
|
|
|
|
|
|
|
252
|
0
|
|
|
|
|
|
my $r=$self->simple_request($post);
|
253
|
0
|
0
|
|
|
|
|
unless ($r->is_success) {
|
254
|
0
|
|
|
|
|
|
croak "Authentication failure ".$r->decoded_content;
|
255
|
|
|
|
|
|
|
}
|
256
|
|
|
|
|
|
|
|
257
|
0
|
|
|
|
|
|
my $data=from_json($r->decoded_content);
|
258
|
0
|
|
|
|
|
|
for (keys %$data) {
|
259
|
0
|
|
|
|
|
|
$self->{$_}=$data->{$_};
|
260
|
|
|
|
|
|
|
}
|
261
|
|
|
|
|
|
|
|
262
|
0
|
|
|
|
|
|
$self->{expires}=(time + $data->{expires_in});
|
263
|
0
|
0
|
|
|
|
|
$self->writestore() if ($self->{presistent});
|
264
|
0
|
|
|
|
|
|
$self->default_header('Authorization' => "Bearer ".$self->{access_token});
|
265
|
|
|
|
|
|
|
|
266
|
0
|
|
|
|
|
|
return $data->{access_token};
|
267
|
|
|
|
|
|
|
}
|
268
|
|
|
|
|
|
|
|
269
|
|
|
|
|
|
|
sub get {
|
270
|
|
|
|
|
|
|
|
271
|
0
|
|
|
0
|
1
|
|
my ($self,@params)=@_;
|
272
|
|
|
|
|
|
|
|
273
|
0
|
|
|
|
|
|
return $self->request('GET',@params);
|
274
|
|
|
|
|
|
|
}
|
275
|
|
|
|
|
|
|
|
276
|
|
|
|
|
|
|
sub post {
|
277
|
0
|
|
|
0
|
1
|
|
my ($self,@params)=@_;
|
278
|
|
|
|
|
|
|
|
279
|
0
|
|
|
|
|
|
return $self->request('POST',@params);
|
280
|
|
|
|
|
|
|
|
281
|
|
|
|
|
|
|
}
|
282
|
|
|
|
|
|
|
|
283
|
|
|
|
|
|
|
sub head {
|
284
|
0
|
|
|
0
|
1
|
|
my ($self,@params)=@_;
|
285
|
|
|
|
|
|
|
|
286
|
0
|
|
|
|
|
|
return $self->request('HEAD',@params);
|
287
|
|
|
|
|
|
|
|
288
|
|
|
|
|
|
|
}
|
289
|
|
|
|
|
|
|
|
290
|
|
|
|
|
|
|
sub patch {
|
291
|
0
|
|
|
0
|
1
|
|
my ($self,@params)=@_;
|
292
|
|
|
|
|
|
|
|
293
|
0
|
|
|
|
|
|
return $self->request('PATCH',@params);
|
294
|
|
|
|
|
|
|
|
295
|
|
|
|
|
|
|
}
|
296
|
|
|
|
|
|
|
|
297
|
|
|
|
|
|
|
sub put {
|
298
|
0
|
|
|
0
|
1
|
|
my ($self,@params)=@_;
|
299
|
|
|
|
|
|
|
|
300
|
0
|
|
|
|
|
|
return $self->request('PUT',@params);
|
301
|
|
|
|
|
|
|
|
302
|
|
|
|
|
|
|
}
|
303
|
|
|
|
|
|
|
|
304
|
|
|
|
|
|
|
sub delete {
|
305
|
0
|
|
|
0
|
1
|
|
my ($self,@params)=@_;
|
306
|
|
|
|
|
|
|
|
307
|
0
|
|
|
|
|
|
return $self->request('DELETE',@params);
|
308
|
|
|
|
|
|
|
|
309
|
|
|
|
|
|
|
}
|
310
|
|
|
|
|
|
|
|
311
|
|
|
|
|
|
|
package LWP::UserAgent::msgraph::srvauth;
|
312
|
1
|
|
|
1
|
|
9
|
use base 'HTTP::Server::Simple::CGI';
|
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
515
|
|
313
|
1
|
|
|
1
|
|
9883
|
use HTTP::Server::Simple::CGI;
|
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
44
|
|
314
|
1
|
|
|
1
|
|
6
|
use IO::Socket qw(AF_INET AF_UNIX SOCK_STREAM SHUT_WR);
|
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
6
|
|
315
|
|
|
|
|
|
|
|
316
|
|
|
|
|
|
|
sub valid_http_method($$) {
|
317
|
|
|
|
|
|
|
|
318
|
0
|
|
|
0
|
|
|
my ($self,$method)=@_;
|
319
|
0
|
|
|
|
|
|
return ($method eq 'GET');
|
320
|
|
|
|
|
|
|
}
|
321
|
|
|
|
|
|
|
sub setcaller($$$) {
|
322
|
|
|
|
|
|
|
|
323
|
0
|
|
|
0
|
|
|
my $self=shift();
|
324
|
0
|
|
|
|
|
|
my $ms=shift();
|
325
|
0
|
|
|
|
|
|
my $port=shift();
|
326
|
|
|
|
|
|
|
|
327
|
0
|
|
|
|
|
|
$self->{'code_uri'}=$ms->authendpoint();
|
328
|
0
|
|
|
|
|
|
$self->{'callerport'}=$port;
|
329
|
0
|
|
|
|
|
|
return 1;
|
330
|
|
|
|
|
|
|
}
|
331
|
|
|
|
|
|
|
|
332
|
|
|
|
|
|
|
sub sendcode($$$) {
|
333
|
|
|
|
|
|
|
|
334
|
0
|
|
|
0
|
|
|
my ($self,$code,$state)=@_;
|
335
|
|
|
|
|
|
|
|
336
|
|
|
|
|
|
|
my $client = IO::Socket->new(
|
337
|
|
|
|
|
|
|
Domain => AF_INET,
|
338
|
|
|
|
|
|
|
Type => SOCK_STREAM,
|
339
|
|
|
|
|
|
|
proto => 'tcp',
|
340
|
|
|
|
|
|
|
PeerPort => $self->{callerport},
|
341
|
0
|
|
0
|
|
|
|
PeerHost => '127.0.0.1',
|
342
|
|
|
|
|
|
|
) || die "Can't open socket: $IO::Socket::errstr";
|
343
|
|
|
|
|
|
|
|
344
|
0
|
|
|
|
|
|
$client->send($state.' '.$code);
|
345
|
0
|
|
|
|
|
|
$client->shutdown(SHUT_WR);
|
346
|
0
|
|
|
|
|
|
$client->close();
|
347
|
|
|
|
|
|
|
}
|
348
|
|
|
|
|
|
|
|
349
|
|
|
|
|
|
|
#Here we setup a minimal web server response behavior
|
350
|
|
|
|
|
|
|
#The only verbs allowed are:
|
351
|
|
|
|
|
|
|
# GET /start ==> does a 302 redirect to the MS authorization platform
|
352
|
|
|
|
|
|
|
# GET /auth ==> receives the authorization code in the query string
|
353
|
|
|
|
|
|
|
#
|
354
|
|
|
|
|
|
|
# This two methods performs an MS challenge to the end-user
|
355
|
|
|
|
|
|
|
#
|
356
|
|
|
|
|
|
|
# Note that depending on your particular browser state, there could be
|
357
|
|
|
|
|
|
|
# a valid MS tenant session already logged in with this app previously
|
358
|
|
|
|
|
|
|
# authorized. In that case, the user doesn't get the login challenge
|
359
|
|
|
|
|
|
|
# and the only thing the browser performs is a series of redirects
|
360
|
|
|
|
|
|
|
# In that case, the authorization code get to us in a blink-you-missed-it
|
361
|
|
|
|
|
|
|
# fashion
|
362
|
|
|
|
|
|
|
sub handle_request {
|
363
|
0
|
|
|
0
|
|
|
my $self = shift;
|
364
|
0
|
|
|
|
|
|
my $cgi = shift;
|
365
|
|
|
|
|
|
|
|
366
|
0
|
|
|
|
|
|
my $path = $cgi->request_uri();
|
367
|
|
|
|
|
|
|
|
368
|
0
|
0
|
|
|
|
|
if ($path =~ "^/auth" ) {
|
|
|
0
|
|
|
|
|
|
369
|
0
|
|
|
|
|
|
print "HTTP/1.0 200 OK\r\n";
|
370
|
0
|
|
|
|
|
|
my $msg="Authentication ok. You can close this window now.\n";
|
371
|
0
|
|
|
|
|
|
print $cgi->header(-type=>'text/plain', -Content_length => length($msg));
|
372
|
0
|
|
|
|
|
|
my $code=$cgi->param('code');
|
373
|
0
|
|
|
|
|
|
my $state=$cgi->param('state');
|
374
|
0
|
|
|
|
|
|
$self->sendcode($code,$state);
|
375
|
0
|
|
|
|
|
|
print $msg;
|
376
|
|
|
|
|
|
|
|
377
|
0
|
|
|
|
|
|
exit 0;
|
378
|
|
|
|
|
|
|
} elsif ($path =~ "^/start" ) {
|
379
|
0
|
|
|
|
|
|
print "HTTP/1.0 302 Redirected\r\n";
|
380
|
0
|
|
|
|
|
|
print $cgi->redirect($self->{'code_uri'});
|
381
|
|
|
|
|
|
|
}
|
382
|
|
|
|
|
|
|
else {
|
383
|
0
|
|
|
|
|
|
print "HTTP/1.0 404 Not found\r\n";
|
384
|
0
|
|
|
|
|
|
print $cgi->header,
|
385
|
|
|
|
|
|
|
$cgi->start_html('Not found'),
|
386
|
|
|
|
|
|
|
$cgi->h1('Not found'),
|
387
|
|
|
|
|
|
|
$cgi->end_html;
|
388
|
|
|
|
|
|
|
}
|
389
|
|
|
|
|
|
|
}
|
390
|
|
|
|
|
|
|
|
391
|
|
|
|
|
|
|
sub print_banner($) {
|
392
|
0
|
|
|
0
|
|
|
my $self=shift();
|
393
|
|
|
|
|
|
|
|
394
|
0
|
|
|
|
|
|
my $url="http://localhost:".$self->port()."/start";
|
395
|
0
|
|
|
|
|
|
print "Authentication required.\nOpen your browser at $url\n";
|
396
|
|
|
|
|
|
|
|
397
|
|
|
|
|
|
|
}
|
398
|
|
|
|
|
|
|
|
399
|
|
|
|
|
|
|
|
400
|
|
|
|
|
|
|
|
401
|
|
|
|
|
|
|
1;
|
402
|
|
|
|
|
|
|
|
403
|
|
|
|
|
|
|
=pod
|
404
|
|
|
|
|
|
|
|
405
|
|
|
|
|
|
|
=encoding UTF-8
|
406
|
|
|
|
|
|
|
|
407
|
|
|
|
|
|
|
=head1 NAME
|
408
|
|
|
|
|
|
|
|
409
|
|
|
|
|
|
|
LWP::UserAgent::msgraph
|
410
|
|
|
|
|
|
|
|
411
|
|
|
|
|
|
|
=head1 VERSION
|
412
|
|
|
|
|
|
|
|
413
|
|
|
|
|
|
|
version 0.01
|
414
|
|
|
|
|
|
|
|
415
|
|
|
|
|
|
|
=head1 SYNOPSIS
|
416
|
|
|
|
|
|
|
|
417
|
|
|
|
|
|
|
use LWP::UserAgent::msgraph;
|
418
|
|
|
|
|
|
|
|
419
|
|
|
|
|
|
|
#The XXXX, YYYY and ZZZZ are from your Azure App Registration
|
420
|
|
|
|
|
|
|
#Application Permission version
|
421
|
|
|
|
|
|
|
$ua = LWP::UserAgent::msgraph->new(
|
422
|
|
|
|
|
|
|
appid => 'XXXX',
|
423
|
|
|
|
|
|
|
secret => 'YYYY',
|
424
|
|
|
|
|
|
|
tenant => 'ZZZZ',
|
425
|
|
|
|
|
|
|
grant_type => 'client_credentials');
|
426
|
|
|
|
|
|
|
$joe=$ua->request(GET => '/users/jdoe@some.com');
|
427
|
|
|
|
|
|
|
$dn=$joe->{displayName};
|
428
|
|
|
|
|
|
|
|
429
|
|
|
|
|
|
|
=head1 DESCRIPTION
|
430
|
|
|
|
|
|
|
|
431
|
|
|
|
|
|
|
This module allows the interaction between Perl and the MS Graph API service.
|
432
|
|
|
|
|
|
|
Therefore, a MS Graph application can be built using Perl. The application must
|
433
|
|
|
|
|
|
|
be correctly registered within Azure with the proper persmissions.
|
434
|
|
|
|
|
|
|
|
435
|
|
|
|
|
|
|
This module has the glue for the needed authentication scheme and the JSON
|
436
|
|
|
|
|
|
|
serialization so a conversation can be established with MS Graph. This is only
|
437
|
|
|
|
|
|
|
middleware. No higher level object abstraction is provided for the MS Graph
|
438
|
|
|
|
|
|
|
object data.
|
439
|
|
|
|
|
|
|
|
440
|
|
|
|
|
|
|
=head1 CONSTRUCTOR
|
441
|
|
|
|
|
|
|
|
442
|
|
|
|
|
|
|
my $ua=LWP::UserAgent->new(%options);
|
443
|
|
|
|
|
|
|
|
444
|
|
|
|
|
|
|
This method constructs a new L object.
|
445
|
|
|
|
|
|
|
key/value pairs must be supplied in order to setup the object
|
446
|
|
|
|
|
|
|
properly. Missing mandatory options will result in error
|
447
|
|
|
|
|
|
|
|
448
|
|
|
|
|
|
|
KEY MEANING
|
449
|
|
|
|
|
|
|
------- -----------------------------------
|
450
|
|
|
|
|
|
|
appid Application (client) ID
|
451
|
|
|
|
|
|
|
secret shared secret needed for handshake
|
452
|
|
|
|
|
|
|
tenant Tenant id
|
453
|
|
|
|
|
|
|
grant_type Authorizations scheme (client_credentials,authorization_code)
|
454
|
|
|
|
|
|
|
console Indicates whether interaction with a user is possible
|
455
|
|
|
|
|
|
|
redirect_uri Redirect URI for delegated auth challenge
|
456
|
|
|
|
|
|
|
local_port tcp port for mini http server. Defaults to 8081
|
457
|
|
|
|
|
|
|
|
458
|
|
|
|
|
|
|
=head1 auth
|
459
|
|
|
|
|
|
|
|
460
|
|
|
|
|
|
|
my $token = $ua->auth; #For app credentiales
|
461
|
|
|
|
|
|
|
my $token = $ua->auth($challenge); #For delegated authentication
|
462
|
|
|
|
|
|
|
|
463
|
|
|
|
|
|
|
This method performs the authentication handshake sequence with the MS
|
464
|
|
|
|
|
|
|
Graph platform. The optional parameter is the authorization code obtained
|
465
|
|
|
|
|
|
|
from a challenge with the impersonated user. If this is an application only
|
466
|
|
|
|
|
|
|
non-delegated client, then the $challenge is not needed.
|
467
|
|
|
|
|
|
|
|
468
|
|
|
|
|
|
|
If used in a web application, you should have redirected the user to the L location
|
469
|
|
|
|
|
|
|
and then capture the resulting code listening for the redirect_uri.
|
470
|
|
|
|
|
|
|
|
471
|
|
|
|
|
|
|
A special tweak is supplied for console applications with delegated authentication. In that case,
|
472
|
|
|
|
|
|
|
if the code is missing, an http localhost miniserver is launched so the
|
473
|
|
|
|
|
|
|
user can trigger the challenge himself. This behavior is activated via the console constructor option.
|
474
|
|
|
|
|
|
|
The http miniserver is destroyed as soon as the authorization code arrives.
|
475
|
|
|
|
|
|
|
In this case, the redirect_uri is automatically set. The miniserver listens by default on http://localhost:8081.
|
476
|
|
|
|
|
|
|
Please note that MS Graph allows
|
477
|
|
|
|
|
|
|
the use of localhost in the redirect_uri and in that case SSL is not enforced. But still the
|
478
|
|
|
|
|
|
|
localhost URL must be registered in Azure.
|
479
|
|
|
|
|
|
|
|
480
|
|
|
|
|
|
|
=head1 request
|
481
|
|
|
|
|
|
|
|
482
|
|
|
|
|
|
|
my $object=$ua->request(GET => '/me');
|
483
|
|
|
|
|
|
|
$ua->request(PATCH => '/me', {officeLocation => $mynewoffice});
|
484
|
|
|
|
|
|
|
|
485
|
|
|
|
|
|
|
The request method makes a call to a MS Graph endpoint url and returns the
|
486
|
|
|
|
|
|
|
corresponding response object. An optional perl structure might be
|
487
|
|
|
|
|
|
|
supplied as the payload (body) for the request.
|
488
|
|
|
|
|
|
|
|
489
|
|
|
|
|
|
|
The MS Graph has a rich set of API calls for different operations. Check the
|
490
|
|
|
|
|
|
|
L section for more tips.
|
491
|
|
|
|
|
|
|
|
492
|
|
|
|
|
|
|
=head1 code
|
493
|
|
|
|
|
|
|
|
494
|
|
|
|
|
|
|
print "It worked" if ($ua->code == 201);
|
495
|
|
|
|
|
|
|
|
496
|
|
|
|
|
|
|
A code() method is supplied as a convenient way of getting the last HTTP response
|
497
|
|
|
|
|
|
|
code.
|
498
|
|
|
|
|
|
|
|
499
|
|
|
|
|
|
|
=head1 next
|
500
|
|
|
|
|
|
|
|
501
|
|
|
|
|
|
|
$more=$ua->next();
|
502
|
|
|
|
|
|
|
|
503
|
|
|
|
|
|
|
The next() method will request additional response content after a previous
|
504
|
|
|
|
|
|
|
request if a pagination result set happens.
|
505
|
|
|
|
|
|
|
|
506
|
|
|
|
|
|
|
=head1 authendpoint
|
507
|
|
|
|
|
|
|
|
508
|
|
|
|
|
|
|
$location=$ua->authendpoint()
|
509
|
|
|
|
|
|
|
|
510
|
|
|
|
|
|
|
Returns the authentication endpoint as an url string, full with the query part. In a delegated
|
511
|
|
|
|
|
|
|
authentication mode, you should point the user to this url via a browser in order to get the proper
|
512
|
|
|
|
|
|
|
authorization. This is on offline method, the resulting uri is computed from the constructor options
|
513
|
|
|
|
|
|
|
|
514
|
|
|
|
|
|
|
=head1 tokenendpoint
|
515
|
|
|
|
|
|
|
|
516
|
|
|
|
|
|
|
$location=$ua->tokenendpoint()
|
517
|
|
|
|
|
|
|
|
518
|
|
|
|
|
|
|
Returns the oauth 2.0 token endpoint as an url string. This url is used internally to get
|
519
|
|
|
|
|
|
|
the authentication token.
|
520
|
|
|
|
|
|
|
|
521
|
|
|
|
|
|
|
=head1 Changes from the default LWP::UserAgent behavior
|
522
|
|
|
|
|
|
|
|
523
|
|
|
|
|
|
|
This class inherits from L, but some changes apply. If you are used to
|
524
|
|
|
|
|
|
|
LWP::UserAgent standart tweaks and shortcuts, you should read this.
|
525
|
|
|
|
|
|
|
|
526
|
|
|
|
|
|
|
The L now accepts a perl structure which will be sent
|
527
|
|
|
|
|
|
|
as a JSON body to the MS Graph endoint. Instead of an L
|
528
|
|
|
|
|
|
|
object, request() will return whatever object is returned by the
|
529
|
|
|
|
|
|
|
MS Graph method, as a perl structure. The module is used as
|
530
|
|
|
|
|
|
|
a serialization engine.
|
531
|
|
|
|
|
|
|
|
532
|
|
|
|
|
|
|
request() will use the right Authorization header based on the initial handshake.
|
533
|
|
|
|
|
|
|
The get(), post(), patch(), delete(), put(), delete() methods are setup so
|
534
|
|
|
|
|
|
|
they call the LWP::UserAgent::msgraph version of request(). That is, they would
|
535
|
|
|
|
|
|
|
return a perl structure according to the MS Graph method.
|
536
|
|
|
|
|
|
|
In particular, post() and patch() accepts a perl structure
|
537
|
|
|
|
|
|
|
as the body. All the binding with the L module has been broken.
|
538
|
|
|
|
|
|
|
|
539
|
|
|
|
|
|
|
The simple_request() method is kept unchanged, but will use the
|
540
|
|
|
|
|
|
|
right Bearer token authentication. So, if you need more control over the request, you can use
|
541
|
|
|
|
|
|
|
this method. You must add the JSON serialization, though.
|
542
|
|
|
|
|
|
|
|
543
|
|
|
|
|
|
|
|
544
|
|
|
|
|
|
|
|
545
|
|
|
|
|
|
|
=cut
|