Add OAuth

This commit is contained in:
mschuepbach
2024-03-27 15:25:17 +01:00
parent 088fdb2b16
commit 8c0319f293
6 changed files with 398 additions and 27 deletions

View File

@@ -0,0 +1,109 @@
package YTMusicAPI::Auth::OAuth::OAuthCredentials;
use strict;
use warnings;
use JSON;
use LWP::UserAgent;
use Data::Dumper;
use YTMusicAPI::Constants qw(
OAUTH_CLIENT_ID
OAUTH_CLIENT_SECRET
OAUTH_CODE_URL
OAUTH_SCOPE
OAUTH_TOKEN_URL
OAUTH_USER_AGENT
);
sub new {
my ( $class, $args ) = @_;
unless ( defined $args->{client_id} == defined $args->{client_secret} ) {
die "OAuthCredential init failure. "
. "Provide both client_id and client_secret or neither.";
}
my $self = {};
bless $self, $class;
$self->{client_id} =
$args->{client_id} ? $args->{client_id} : OAUTH_CLIENT_ID;
$self->{client_secret} =
$args->{client_secret} ? $args->{client_secret} : OAUTH_CLIENT_SECRET;
$self->{_session} =
$args->{session} ? $args->{session} : LWP::UserAgent->new;
if ( $args->{proxies} ) {
@$self->{_session}{ keys %{ $args->{proxies} } } =
values %{ $args->{proxies} };
}
return $self;
}
sub get_code {
my ($self) = @_;
my $code_response =
$self->_send_request( OAUTH_CODE_URL, { "scope" => OAUTH_SCOPE } );
return decode_json( $code_response->decoded_content );
}
sub _send_request {
my ( $self, $url, $data ) = @_;
$data->{"client_id"} = $self->{client_id};
my $request = HTTP::Request->new( POST => $url );
$request->header( "User-Agent" => OAUTH_USER_AGENT );
$request->content( encode_json($data) );
my $response = $self->{_session}->request($request);
if ( $response->code == 401 ) {
my $data = $response->decode_json;
my $issue = $data->{"error"};
if ( $issue eq "unauthorized_client" ) {
die "Token refresh error. Most likely client/token mismatch.";
}
elsif ( $issue eq "invalid_client" ) {
die "OAuth client failure. Most likely client_id and client_secret "
. "mismatch or YouTubeData API is not enabled.";
}
else {
die "OAuth request error. status_code: "
. $response->code
. ", url: $url, content: "
. Dumper($data);
}
}
return $response;
}
sub token_from_code {
my ( $self, $device_code ) = @_;
my $response = $self->_send_request(
OAUTH_TOKEN_URL,
{
"client_secret" => $self->{client_secret},
"grant_type" => "http://oauth.net/grant_type/device/1.0",
"code" => $device_code,
}
);
return decode_json( $response->decoded_content );
}
sub refresh_token {
my ( $self, $refresh_token ) = @_;
my $response = $self->_send_request(
OAUTH_TOKEN_URL,
{
"client_secret" => $self->{client_secret},
"grant_type" => "refresh_token",
"refresh_token" => $refresh_token,
}
);
return decode_json( $response->decoded_content );
}
1;

View File

@@ -0,0 +1,51 @@
package YTMusicAPI::Auth::OAuth::OAuthToken;
use strict;
use warnings;
use JSON qw(decode_json);
use Path::Tiny qw(path);
use parent 'YTMusicAPI::Auth::OAuth::Token';
sub new {
my ( $class, $args ) = @_;
my $self = $class->SUPER::new($args);
bless $self, $class;
return $self;
}
sub is_oauth {
my ($headers) = @_;
my @required_keys = YTMusicAPI::Auth::OAuth::Token::members();
foreach my $key (@required_keys) {
return 0 unless exists $headers->{$key};
}
return 1;
}
sub update {
my ( $self, $fresh_access ) = @_;
$self->{access_token} = $fresh_access->{access_token};
$self->{expires_at} = int( time() ) + $fresh_access->{expires_in};
}
sub is_expiring {
my ($self) = @_;
return $self->{expires_at} - int( time() ) < 60 ? 1 : 0;
}
sub from_json {
my ( $class, $file_path ) = @_;
my $file_pack;
if ( path($file_path)->is_file ) {
$file_pack = decode_json( path($file_path)->slurp_utf8 );
}
return $class->new(%$file_pack);
}
1;

View File

@@ -0,0 +1,80 @@
package YTMusicAPI::Auth::OAuth::RefreshingToken;
use strict;
use warnings;
use parent 'YTMusicAPI::Auth::OAuth::OAuthToken';
use JSON qw(encode_json);
use Path::Tiny qw(path);
sub new {
my ( $class, $credentials, $local_cache, $args ) = @_;
my $self = $class->SUPER::new($args);
$self->{credentials} = $credentials;
$self->{_local_cache} = $local_cache;
return $self;
}
sub get_attribute {
my ( $self, $item ) = @_;
if ( $item eq "access_token" && $self->is_expiring ) {
my $fresh =
$self->{credentials}->refresh_token( $self->{refresh_token} );
$self->update($fresh);
$self->store_token();
}
return $self->{$item};
}
sub local_cache {
my $self = shift;
if (@_) {
$self->{_local_cache} = shift;
$self->store_token();
}
return $self->{_local_cache};
}
sub prompt_for_token {
my ( $class, $credentials, $open_browser, $to_file ) = @_;
$open_browser //= 0;
my $code = $credentials->get_code();
my $url = "$code->{verification_url}?user_code=$code->{user_code}";
if ($open_browser) {
if ( $^O eq 'MSWin32' ) {
system("start $url");
}
elsif ( $^O eq 'darwin' ) {
system("open $url");
}
elsif ( $^O eq 'linux' ) {
system("xdg-open $url");
}
else {
warn "Unable to open url. Unsupported OS";
}
}
print
"Go to $url, finish the login flow and press Enter when done, Ctrl-C to abort\n";
<STDIN>;
my $raw_token = $credentials->token_from_code( $code->{device_code} );
my $ref_token = $class->new( %$raw_token, credentials => $credentials );
$ref_token->update( $ref_token->as_dict() );
if ($to_file) {
$ref_token->local_cache($to_file);
}
return $ref_token;
}
sub store_token {
my ( $self, $path ) = @_;
$path //= $self->local_cache();
if ($path) {
path($path)->spew_utf8( encode_json( $self->as_dict() ) );
}
}
1;

View File

@@ -0,0 +1,61 @@
package YTMusicAPI::Auth::OAuth::Token;
use strict;
use warnings;
use JSON;
sub new {
my ( $class, $args ) = @_;
my $self = {
scope => $args->{scope} // "https://www.googleapis.com/auth/youtube",
token_type => $args->{token_type} // "Bearer",
access_token => $args->{access_token} // undef,
refresh_token => $args->{refresh_token} // undef,
expires_at => $args->{expires_at} // 0,
expires_in => $args->{expires_in} // 0,
};
bless $self, $class;
return $self;
}
sub members {
return qw(
scope
token_type
access_token
refresh_token
expires_at
expires_in
);
}
sub repr {
my ($self) = @_;
return ref($self) . ": " . $self->as_json();
}
sub as_dict {
my ($self) = @_;
my %copy;
@copy{ $self->members() } = @{$self}{ members() };
return \%copy;
}
sub as_json {
my ($self) = @_;
my $dict = $self->as_dict();
return encode_json($dict);
}
sub as_auth {
my ($self) = @_;
return $self->{token_type} . " " . $self->{access_token};
}
sub is_expiring {
my ($self) = @_;
return $self->{expires_in} < 60 ? 1 : 0;
}
1;