From Code to Community: Sponsoring The Perl and Raku Conference 2025 Learn more

use strict;
use utf8;
=encoding utf8
=head1 NAME
WWW::Suffit::Server::API::V1 - The public Suffit API controller, version 1
=head1 SYNOPSIS
use WWW::Suffit::Server::API::V1;
=head1 DESCRIPTION
The public Suffit API controller, version 1
=head1 METHODS
List of internal methods
=head2 authn
See L</"POST /api/v1/authn">
=head2 authz
See L</"POST /api/v1/authz">
=head2 public_key
See L</"GET /api/v1/publicKey">
=head1 API METHODS
List of API methods
=head2 POST /api/v1/authn
This method performs authentication of the remote user
# curl -v -H "Authorization: Bearer eyJh...Ggns" \
-X POST -d '{
"username": "bob",
"password": "bob",
"address": "1.2.3.4",
"encrypted": false
}' \
> POST /api/v1/authn HTTP/1.1
> Host: localhost:8695
> User-Agent: curl/7.68.0
> Accept: */*
> Authorization: Bearer eyJh...Ggns
> Content-Length: 120
> Content-Type: application/x-www-form-urlencoded
>
< HTTP/1.1 200 OK
< Content-Length: 30
< Content-Type: application/json;charset=UTF-8
< Date: Wed, 14 Aug 2024 10:18:22 GMT
< Server: OWL/1.11
<
{
"code": "E0000",
"status": true
}
# curl -v -H "Authorization: Bearer eyJh...Ggns" \
-X POST -d '{
"username": "bob",
"password": "incorrect",
"address": "1.2.3.4",
"encrypted": false
}' \
> POST /api/v1/authn HTTP/1.1
> Host: localhost:8695
> User-Agent: curl/7.68.0
> Accept: */*
> Authorization: Bearer eyJh...Ggns
> Content-Length: 126
> Content-Type: application/x-www-form-urlencoded
>
< HTTP/1.1 401 Unauthorized
< Content-Length: 74
< Content-Type: application/json;charset=UTF-8
< Date: Wed, 14 Aug 2024 10:22:11 GMT
< Server: OWL/1.11
<
{
"code": "E1326",
"message": "Incorrect username or password",
"status": false
}
=head2 POST /api/v1/authz
This method performs authorization and checks access grants of the remote user
# curl -v -H "Authorization: Bearer eyJh...Ggns" \
-X POST -d '{
"username": "bob",
"method": "GET",
"address": "1.2.3.4",
"headers": {
"Accept": "text/html,text/plain",
"Connection": "keep-alive",
"Host": "localhost:8695"
}
}' \
> POST /api/v1/authz HTTP/1.1
> Host: localhost:8695
> User-Agent: curl/7.68.0
> Accept: */*
> Authorization: Bearer eyJh...Ggns
> Content-Length: 296
> Content-Type: application/x-www-form-urlencoded
>
< HTTP/1.1 200 OK
< Content-Length: 30
< Content-Type: application/json;charset=UTF-8
< Date: Wed, 14 Aug 2024 10:40:17 GMT
< Server: OWL/1.11
<
{
"code": "E0000",
"status": true
}
# curl -v -H "Authorization: Bearer eyJh...Ggns" \
-X POST -d '{
"username": "bob",
"method": "GET",
"address": "1.2.3.4",
"verbose": true
}' \
> POST /api/v1/authz HTTP/1.1
> Host: localhost:8695
> User-Agent: curl/7.68.0
> Accept: */*
> Authorization: Bearer eyJh...Ggns
> Content-Length: 166
> Content-Type: application/x-www-form-urlencoded
>
< HTTP/1.1 200 OK
< Content-Length: 289
< Content-Type: application/json;charset=UTF-8
< Date: Wed, 14 Aug 2024 10:56:45 GMT
< Server: OWL/1.11
<
{
"address": "1.2.3.4",
"code": "E0000",
"email": "bob@example.com",
"email_md5": "4b9bb80620f03eb3719e0a061c14283d",
"expires": 1723633005,
"groups": [],
"method": "GET",
"name": "Bob Bob",
"path": "/api/check",
"role": "Test user",
"status": true,
"uid": 13,
"username": "bob"
}
# curl -v -H "Authorization: Bearer eyJh...Ggns" \
-X POST -d '{
"username": "unknown",
"method": "GET",
"address": "1.2.3.4"
}' \
> POST /api/v1/authz HTTP/1.1
> Host: localhost:8695
> User-Agent: curl/7.68.0
> Accept: */*
> Authorization: Bearer eyJh...Ggns
> Content-Length: 145
> Content-Type: application/x-www-form-urlencoded
>
< HTTP/1.1 401 Unauthorized
< Content-Length: 58
< Content-Type: application/json;charset=UTF-8
< Date: Wed, 14 Aug 2024 10:43:43 GMT
< Server: OWL/1.11
<
{
"code": "E1310",
"message": "User not found",
"status": false
}
=head2 GET /api/v1/publicKey
Get RSA public key of user (of the token issuer)
# curl -v -H "Authorization: Bearer eyJh...Ggns" \
> GET /api/v1/publicKey HTTP/1.1
> Host: localhost:8695
> User-Agent: curl/7.68.0
> Accept: */*
> Authorization: Bearer eyJh...Ggns
>
< HTTP/1.1 200 OK
< Content-Length: 306
< Content-Type: application/json;charset=UTF-8
< Date: Wed, 14 Aug 2024 11:02:33 GMT
< Server: OWL/1.11
<
{
"code": "E0000",
"public_key": "-----BEGIN RSA PUBLIC KEY-----...",
"status": true
}
=head1 ERROR CODES
List of V1 Suffit API error codes
API | HTTP | DESCRIPTION
-------+-------+-------------------------------------------------
E1100 [404] No RSA public key found
E1101 [400] No RSA private key found
E1102 [500] RSA decrypt error
E1103 [ * ] Incorrect username or password
E1104 [ * ] Access denied (authz)
E1105 [ * ] Access denied by realm restrictions (access)
E1106 [---] Reserved
E1107 [---] Reserved
E1108 [---] Reserved
E1109 [---] Reserved
B<*> -- this code will be defined later on the interface side
See also list of common Suffit API error codes in L<WWW::Suffit::API/"ERROR CODES">
=head1 HISTORY
See C<Changes> file
=head1 TO DO
See C<TODO> file
=head1 SEE ALSO
L<Mojolicious>, L<WWW::Suffit>, L<WWW::Suffit::Server>, L<WWW::Suffit::API>
=head1 AUTHOR
Serż Minus (Sergey Lepenkov) L<https://www.serzik.com> E<lt>abalama@cpan.orgE<gt>
=head1 COPYRIGHT
Copyright (C) 1998-2024 D&D Corporation. All Rights Reserved
=head1 LICENSE
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
See C<LICENSE> file and L<https://dev.perl.org/licenses/>
=cut
use Acrux::RefUtil qw/ is_hash_ref /;
sub authn {
my $self = shift;
my $username = $self->req->json('/username') // '';
my $password = $self->req->json('/password') // '';
my $address = $self->req->json('/address') // '';
my $encrypted = $self->req->json('/encrypted') || 0;
my $cachekey = $self->stash('cachekey');
my $authdb = $self->authdb;
my $acc_user = $authdb->cached_user($self->stash('username'), $cachekey);
my $public_key = $acc_user->public_key // '';
my $private_key = $acc_user->private_key // '';
$authdb->clean;
# Password decrypt
if ($encrypted && length($password)) {
return $self->reply->json_error(400 => "E1100" => "No RSA public key found") unless length $public_key;
return $self->reply->json_error(400 => "E1101" => "No RSA private key found") unless length $private_key;
my $rsa = WWW::Suffit::RSA->new->private_key($private_key);
$password = $rsa->decrypt($password); # RSA Decrypt password
return $self->reply->json_error(500 => "E1102" => $rsa->error || "RSA decrypt error") if $rsa->error;
}
# Authentication
return $self->reply->json_error(
$authdb->code, $authdb->error || "E1103: Incorrect username or password"
) unless $authdb->authn(
u => $username,
p => $password,
k => $cachekey,
a => $address, # For check by stats
); # Unauthenticated
# Render ok
return $self->reply->json_ok;
}
sub authz {
my $self = shift;
my $authdb = $self->authdb->clean;
my $cachekey = $self->stash('cachekey');
my $username = $self->req->json('/username') // '';
my $verbose = $self->req->json('/verbose') || 0;
my $url = $self->req->json('/url') // $self->base_url // '';
my $uri = Mojo::URL->new($url);
$username = $uri->username unless length $username;
my %args = (controller => $self, username => $username, cachekey => $cachekey);
$args{address} = $self->req->json('/address') // '';
$args{method} = $self->req->json('/method') // '';
$args{path} = $self->req->json('/path') //
$uri->path->leading_slash(1)->trailing_slash(0)->to_string // '';
$args{base} = $self->req->json('/base') //
$uri->path_query("")->path(Mojo::Path->new->leading_slash(0)->trailing_slash(0) )->to_string // '';
my $headers = $self->req->json('/headers') || {};
if (is_hash_ref($headers)) {
my %hdrs = ();
while (my ($k, $v) = each %$headers) {
$hdrs{$k} = $v if defined($v) && !ref($v);
}
$args{headers} = {%hdrs} if scalar %hdrs;
}
# Authorization (External!)
my $user = $authdb->authz(
u => $username,
's' => 1,
k => $cachekey,
);
return $self->reply->json_error(
$authdb->code, $authdb->error || "E1104: Access denied"
) unless $user; # Unauthorized
#$self->log->debug($self->dumper($user));
# Access (username is optional)
return $self->reply->json_error(
$authdb->code, $authdb->error || "E1105: Access denied by realm restrictions"
) unless ($authdb->access(%args)); # Forbidden
# Render ok
return $self->reply->json_ok unless $verbose;
# Render user data
my %ret = $user->to_hash;
$ret{address} = $args{address} if length $args{address};
foreach my $k (qw/method path base/) {
$ret{$k} = $args{$k} if length $args{$k};
}
$ret{headers} = $args{headers} if exists $args{headers};
# Ok
return $self->reply->json_ok({%ret});
}
sub public_key {
my $self = shift;
my $username = $self->stash('username');
my $cachekey = $self->stash('cachekey');
my $authdb = $self->authdb;
my $public_key = $authdb->cached_user($username, $cachekey)->public_key // '';
return $self->reply->json_error(404 => "E1100" => "No RSA public key found") unless length $public_key;
return $self->reply->json_ok({public_key => $public_key});
}
1;
__END__