Skip to content

Commit

Permalink
Merge pull request #49 from danielvijge/feature/oauth-2.1
Browse files Browse the repository at this point in the history
Use OAuth 2.1 authentication
  • Loading branch information
danielvijge authored Nov 17, 2024
2 parents 0c62fbb + 421905d commit e27a646
Show file tree
Hide file tree
Showing 7 changed files with 949 additions and 47 deletions.
47 changes: 34 additions & 13 deletions HTML/EN/plugins/SqueezeCloud/settings/basic.html
Original file line number Diff line number Diff line change
@@ -1,30 +1,51 @@
[% PROCESS settings/header.html %]

[% IF code %]
[% WRAPPER setting title="PLUGIN_SQUEEZECLOUD_CONNECT_WITH_SOUNDCLOUD" desc="PLUGIN_SQUEEZECLOUD_CONNECT_WITH_SOUNDCLOUD_DESC" %]
[% "PLUGIN_SQUEEZECLOUD_LOGGING_IN" | string %]
[% END %]
[% ELSIF username %]
[% WRAPPER setting title="PLUGIN_SQUEEZECLOUD_CONNECT_WITH_SOUNDCLOUD" desc="PLUGIN_SQUEEZECLOUD_CONNECT_WITH_SOUNDCLOUD_DESC" %]
[% "PLUGIN_SQUEEZECLOUD_LOGGED_IN_AS" | string %] [% username %]<br/>
<input type="checkbox" class="stdedit" name="logout" />[% "PLUGIN_SQUEEZECLOUD_LOG_OUT" | string %]
[% END %]
[% ELSIF codeChallenge %]
[% WRAPPER setting title="PLUGIN_SQUEEZECLOUD_CONNECT_WITH_SOUNDCLOUD" desc="PLUGIN_SQUEEZECLOUD_CONNECT_WITH_SOUNDCLOUD_DESC" %]
<script src="https://connect.soundcloud.com/sdk/sdk-3.3.2.js"></script>
<script>
SC.initialize({
client_id: '112d35211af80d72c8ff470ab66400d8',
redirect_uri: 'https://danielvijge.github.io/SqueezeCloud/callback.html',
response_type: 'code',
scope: ''
});
function openAuthorizationWindow() {
window.open(
'https://secure.soundcloud.com/authorize?' +
'client_id=112d35211af80d72c8ff470ab66400d8' +
'&redirect_uri=https://danielvijge.github.io/SqueezeCloud/callback.html' +
'&response_type=code' +
'&code_challenge=[% codeChallenge %]' +
'&code_challenge_method=S256' +
'&state=[% hostName %]',
'SoundCloudAuthorizationWindow',
'width=500,height=800'
);
};
</script>
<img src="/plugins/SqueezeCloud/html/images/btn-connect-sc-l.png" alt="Connect with SoundCloud" onclick="SC.connect();" style="cursor: pointer;" />

<img src="/plugins/SqueezeCloud/html/images/btn-connect-sc-l.png" alt="Connect with SoundCloud" onclick="openAuthorizationWindow()" style="cursor: pointer;"/>
[% END %]

[% WRAPPER setting title="PLUGIN_SQUEEZECLOUD_APIKEY" desc="PLUGIN_SQUEEZECLOUD_APIKEY_DESC" %]
[% WRAPPER setting title="PLUGIN_SQUEEZECLOUD_CODE" desc="PLUGIN_SQUEEZECLOUD_CODE_DESC" %]
<div class="prefDesc">
<input type="text" class="stdedit" name="pref_apiKey" value="[% prefs.apiKey %]" size="40" />
<input type="text" class="stdedit" name="code" size="40" />
</div>
[% END %]
[% ELSE %]
[% WRAPPER setting title="PLUGIN_SQUEEZECLOUD_CONNECT_WITH_SOUNDCLOUD" desc="PLUGIN_SQUEEZECLOUD_CONNECT_WITH_SOUNDCLOUD_DESC" %]
[% "PLUGIN_SQUEEZECLOUD_LOGIN_ERROR" | string %]
[% END %]

[% END %]

[% WRAPPER setting title="PLUGIN_SQUEEZECLOUD_PLAYMETHOD" desc="PLUGIN_SQUEEZECLOUD_PLAYMETHOD_DESC" %]
<div class="prefDesc">
<select name="pref_playmethod" class="stdedit">
<option[% IF prefs.playmethod == 'stream' %] selected[% END %] value="stream">Always use stream method</option>
<option[% IF prefs.playmethod == 'download' %] selected[% END %] value="download">Use download method if available</option>
<option[% IF prefs.playmethod == 'stream' %] selected[% END %] value="stream">[% "PLUGIN_SQUEEZECLOUD_USE_STREAM_METHOD" | string %]</option>
<option[% IF prefs.playmethod == 'download' %] selected[% END %] value="download">[% "PLUGIN_SQUEEZECLOUD_USE_DOWNLOAD_METHOD" | string %]</option>
</select>
</div>

Expand Down
202 changes: 202 additions & 0 deletions Oauth2.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package Plugins::SqueezeCloud::Oauth2;

# Plugin to stream audio from SoundCloud streams
#
# Released under GNU General Public License version 2 (GPLv2)
#
# Written by Daniel Vijge
#
# See file LICENSE for full license details

use strict;

use Slim::Utils::Prefs;
use Slim::Utils::Log;
use Slim::Utils::Cache;
use Slim::Utils::Strings qw(string cstring);
use JSON::XS::VersionOneAndTwo;
use Plugins::SqueezeCloud::Random qw(random_regex);
use Digest::SHA qw(sha256_base64);

my $log = logger('plugin.squeezecloud');
my $prefs = preferences('plugin.squeezecloud');
my $cache = Slim::Utils::Cache->new();

use constant CLIENT_ID => "112d35211af80d72c8ff470ab66400d8";
use constant CLIENT_SECRET => "fc63200fee37d02bc3216cfeffe5f5ae";
use constant REDIRECT_URI => "https%3A%2F%2Fdanielvijge.github.io%2FSqueezeCloud%2Fcallback.html";
use constant META_CACHE_TTL => 86400 * 30; # 24 hours x 30 = 30 days

sub isLoggedIn {
return(isRefreshTokenAvailable() || isApiKeyAvailable());
}

sub isApiKeyAvailable {
return ($prefs->get('apiKey') ne '');
}

sub isAccessTokenAvailable {
return ($cache->get('access_token') ne '');
}

sub isRefreshTokenAvailable {
return ($cache->get('refresh_token') ne '');
}

sub isAccessTokenExpired {
return 0 if isApiKeyAvailable(); # API key cannot expire
return 0 if isAccessTokenAvailable(); # Access token still valid
return 1 if isRefreshTokenAvailable(); # Access token expired, refresh token available
return 1; # This should not happen, equal to isLoggedIn() == false
}

sub getAccessToken {
$log->debug('getAccessToken started.');

if (!isRefreshTokenAvailable()) {
$log->error('No authentication available. Use the settings page to log in first.');
return;
}

if (!isAccessTokenAvailable()) {
$log->info('Access token has expired. Getting a new access token with the refresh token.');
getAccessTokenWithRefreshToken(\&getAccessToken, @_);
return;
}

$log->debug('Cached access token ' . $cache->get('access_token'));
return $cache->get('access_token');
}

sub getAuthorizationToken {
$log->debug('getAuthorizationToken started.');

my $code = shift;

if (!$cache->get('codeVerifier')) {
$log->error('No code verifier is available. Reload the page and try to authenticate again.');
return;
}

my $post = "grant_type=authorization_code" .
"&client_id=" . CLIENT_ID .
"&client_secret=" . CLIENT_SECRET .
"&redirect_uri=" . REDIRECT_URI .
"&code_verifier=" . $cache->get('codeVerifier') .
"&code=" . $code;

my $http = Slim::Networking::SimpleAsyncHTTP->new(
sub {
$log->debug('Successful request for authorization_code.');
my $response = shift;
my $result = eval { from_json($response->content) };

$cache->set('access_token', $result->{access_token}, 30);
$cache->set('refresh_token', $result->{refresh_token}, META_CACHE_TTL);
},
sub {
$log->error('Failed request for authorization_code.');
$log->error($_[1]);

my $response = shift;
my $result = eval { from_json($response->content) };
$log->error($result);
},
{
timeout => 15,
}
);
$log->debug($post);
$http->post(
"https://secure.soundcloud.com/oauth/token",
'Content-Type' => 'application/x-www-form-urlencoded',
$post,
);
}

sub getAccessTokenWithRefreshToken {
$log->debug('getAccessTokenWithRefreshToken started.');

my $cb = shift;
my @params = @_;

if (!isRefreshTokenAvailable()) {
$log->error('No authentication available. Use the settings page to log in first.');
return;
}

if (isAccessTokenAvailable()) {
$log->debug('Still an access token available. No need for a refresh.');
return;
}

$log->debug('Cached refresh token ' . $cache->get('refresh_token'));
my $post = "grant_type=refresh_token" .
"&client_id=" . CLIENT_ID .
"&client_secret=" . CLIENT_SECRET .
"&refresh_token=" . $cache->get('refresh_token');

my $http = Slim::Networking::SimpleAsyncHTTP->new(
sub {
$log->debug('Successful request for refresh_token');
my $response = shift;
my $result = eval { from_json($response->content) };
$cache->set('access_token', $result->{access_token}, 30);
$cache->set('refresh_token', $result->{refresh_token}, META_CACHE_TTL);
$cb->(@params) if $cb;
},
sub {
$log->error('Failed request for refresh_token');
$log->error($_[1]);
$log->debug('Removing refresh_token for failed request. User is nog logged out.');
$cache->remove('refresh_token');
$cb->(@params) if $cb;
},
{
timeout => 15,
}
);
$log->debug($post);
$http->post(
"https://secure.soundcloud.com/oauth/token",
'Content-Type' => 'application/x-www-form-urlencoded',
$post,
);
}

sub getAuthenticationHeaders {
$log->debug('getAuthenticationHeaders started.');
if (isApiKeyAvailable()) {
# If there is still an older API key, use this for authentication
$log->debug('Using old API key ' . $prefs->get('apiKey'));
return 'Authorization' => 'OAuth ' . $prefs->get('apiKey');
}
else {
$log->debug('Using bearer token for authorization');
return 'Authorization' => 'Bearer ' . getAccessToken();
}
}

sub getCodeChallenge {
$log->debug('getCodeChallenge started.');
if ($cache->get('codeChallenge')) {
$log->debug('Random string [cached]: '. $cache->get('codeVerifier'));
$log->debug('S256 [cached]: '.$cache->get('codeChallenge'));
return $cache->get('codeChallenge');
}

my $randomString = random_regex('[a-z0-9]{56}');
my $s256 = sha256_base64($randomString);
$s256 =~ s/\+/-/g;
$s256 =~ s/\//_/g;
$s256 =~ s/=$//g;

$log->debug('Random string: '.$randomString);
$log->debug('S256: '.$s256);

$cache->set('codeVerifier', $randomString, 60);
$cache->set('codeChallenge', $s256, 60);
return $s256;
}

1;
Loading

0 comments on commit e27a646

Please sign in to comment.