blob: e2e16f14e7c8980f86502f4fbcff05feebe0b8bf [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "topaz/auth_providers/spotify/spotify_auth_provider_impl.h"
#include <fuchsia/net/oldhttp/cpp/fidl.h>
#include <fuchsia/ui/app/cpp/fidl.h>
#include <lib/fit/function.h>
#include "lib/component/cpp/connect.h"
#include "lib/component/cpp/startup_context.h"
#include "lib/fidl/cpp/interface_request.h"
#include "lib/fxl/functional/make_copyable.h"
#include "lib/fxl/logging.h"
#include "lib/fxl/strings/join_strings.h"
#include "lib/svc/cpp/services.h"
#include "peridot/lib/rapidjson/rapidjson.h"
#include "rapidjson/document.h"
#include "topaz/auth_providers/oauth/oauth_request_builder.h"
#include "topaz/auth_providers/oauth/oauth_response.h"
#include "topaz/auth_providers/spotify/constants.h"
namespace spotify_auth_provider {
namespace http = ::fuchsia::net::oldhttp;
using auth_providers::oauth::OAuthRequestBuilder;
using auth_providers::oauth::ParseOAuthResponse;
using fuchsia::auth::AssertionJWTParams;
using fuchsia::auth::AttestationJWTParams;
using fuchsia::auth::AuthProviderStatus;
using fuchsia::auth::AuthTokenPtr;
using modular::JsonValueToPrettyString;
SpotifyAuthProviderImpl::SpotifyAuthProviderImpl(
component::StartupContext* context,
network_wrapper::NetworkWrapper* network_wrapper,
fidl::InterfaceRequest<fuchsia::auth::AuthProvider> request)
: context_(context),
network_wrapper_(network_wrapper),
binding_(this, std::move(request)) {
FXL_DCHECK(network_wrapper_);
// The class shuts down when the client connection is disconnected.
binding_.set_error_handler([this](zx_status_t status) {
if (on_empty_) {
on_empty_();
}
});
}
SpotifyAuthProviderImpl::~SpotifyAuthProviderImpl() {}
void SpotifyAuthProviderImpl::GetPersistentCredential(
fidl::InterfaceHandle<fuchsia::auth::AuthenticationUIContext>
auth_ui_context,
const fidl::StringPtr user_profile_id,
GetPersistentCredentialCallback callback) {
FXL_DCHECK(auth_ui_context);
get_persistent_credential_callback_ = std::move(callback);
auto view_owner = SetupWebView();
// Set a delegate which will parse incoming URLs for authorization code.
fuchsia::webview::WebRequestDelegatePtr web_request_delegate;
web_request_delegate_bindings_.AddBinding(this,
web_request_delegate.NewRequest());
web_view_->SetWebRequestDelegate(std::move(web_request_delegate));
web_view_->ClearCookies();
// TODO(ukode): use app_scopes instead of |kScopes|
const std::vector<std::string> scopes(kScopes.begin(), kScopes.end());
std::string scopes_str = fxl::JoinStrings(scopes, "+");
std::string url = kSpotifyOAuthAuthEndpoint;
url += "?scope=" + scopes_str;
url += "&response_type=code&redirect_uri=";
url += kRedirectUri;
// TODO: Client_id and secret should be passed as api args for
// GetPersistentCredential. Need to fix the fidl interface in Garnet before
// fixing it here.
url += "&client_id=";
url += "TODO";
web_view_->SetUrl(url);
auth_ui_context_ = auth_ui_context.Bind();
auth_ui_context_.set_error_handler([this](zx_status_t status) {
FXL_VLOG(1) << "Overlay cancelled by the caller";
// close any open web view
if (auth_ui_context_) {
auth_ui_context_.set_error_handler([](zx_status_t status) {});
auth_ui_context_->StopOverlay();
}
auth_ui_context_ = nullptr;
get_persistent_credential_callback_(AuthProviderStatus::INTERNAL_ERROR,
nullptr, nullptr);
return;
});
auth_ui_context_->StartOverlay2(std::move(view_owner));
}
void SpotifyAuthProviderImpl::GetAppAccessToken(
const fidl::StringPtr credential, const fidl::StringPtr app_client_id,
const fidl::VectorPtr<fidl::StringPtr> app_scopes,
GetAppAccessTokenCallback callback) {
if (credential->empty()) {
callback(AuthProviderStatus::BAD_REQUEST, nullptr);
return;
}
if (app_client_id->empty()) {
callback(AuthProviderStatus::BAD_REQUEST, nullptr);
return;
}
auto request = OAuthRequestBuilder(kSpotifyOAuthTokenEndpoint, "POST")
.SetUrlEncodedBody("refresh_token=" + credential.get() +
"&client_id=" + app_client_id.get() +
"&grant_type=refresh_token");
auto request_factory = fxl::MakeCopyable(
[request = std::move(request)] { return request.Build(); });
Request(std::move(request_factory), [callback = std::move(callback)](
http::URLResponse response) {
auto oauth_response = ParseOAuthResponse(std::move(response));
if (oauth_response.status != AuthProviderStatus::OK) {
FXL_VLOG(1) << "Got error: " << oauth_response.error_description;
FXL_VLOG(1) << "Got response: "
<< JsonValueToPrettyString(oauth_response.json_response);
callback(oauth_response.status, nullptr);
return;
}
AuthTokenPtr access_token = fuchsia::auth::AuthToken::New();
access_token->token_type = fuchsia::auth::TokenType::ACCESS_TOKEN;
access_token->token =
oauth_response.json_response["access_token"].GetString();
access_token->expires_in =
oauth_response.json_response["expires_in"].GetUint64();
callback(AuthProviderStatus::OK, std::move(access_token));
});
}
void SpotifyAuthProviderImpl::GetAppIdToken(const fidl::StringPtr credential,
const fidl::StringPtr audience,
GetAppIdTokenCallback callback) {
// Id Tokens are not supported by Spotify.
callback(AuthProviderStatus::BAD_REQUEST, nullptr);
}
void SpotifyAuthProviderImpl::GetAppFirebaseToken(
const fidl::StringPtr id_token, const fidl::StringPtr firebase_api_key,
GetAppFirebaseTokenCallback callback) {
// Firebase Token doesn't exist for Spotify.
callback(AuthProviderStatus::BAD_REQUEST, nullptr);
}
void SpotifyAuthProviderImpl::RevokeAppOrPersistentCredential(
const fidl::StringPtr credential,
RevokeAppOrPersistentCredentialCallback callback) {
// There is no programmatic way to revoke tokens. Instead, Spotify users have
// to manually revoke access from this page here:
// <https://www.spotify.com/account/>
callback(AuthProviderStatus::BAD_REQUEST);
}
void SpotifyAuthProviderImpl::GetPersistentCredentialFromAttestationJWT(
fidl::InterfaceHandle<AttestationSigner> attestation_signer,
AttestationJWTParams jwt_params,
fidl::InterfaceHandle<AuthenticationUIContext> auth_ui_context,
fidl::StringPtr user_profile_id,
GetPersistentCredentialFromAttestationJWTCallback callback) {
// Remote attestation flow not supported.
callback(AuthProviderStatus::BAD_REQUEST, nullptr, nullptr, nullptr, nullptr);
}
void SpotifyAuthProviderImpl::GetAppAccessTokenFromAssertionJWT(
fidl::InterfaceHandle<AttestationSigner> attestation_signer,
AssertionJWTParams jwt_params, fidl::StringPtr credential,
const fidl::VectorPtr<fidl::StringPtr> app_scopes,
GetAppAccessTokenFromAssertionJWTCallback callback) {
// Remote attestation flow not supported.
callback(AuthProviderStatus::BAD_REQUEST, nullptr, nullptr, nullptr);
}
void SpotifyAuthProviderImpl::WillSendRequest(
const fidl::StringPtr incoming_url) {
FXL_DCHECK(get_persistent_credential_callback_);
const std::string& uri = incoming_url.get();
const std::string prefix = std::string{kRedirectUri} + "?code=";
const std::string cancel_prefix =
std::string{kRedirectUri} + "?error=access_denied";
auto cancel_pos = uri.find(cancel_prefix);
// user denied OAuth permissions
if (cancel_pos == 0) {
get_persistent_credential_callback_(AuthProviderStatus::USER_CANCELLED,
nullptr, nullptr);
return;
}
auto pos = uri.find(prefix);
// user performing gaia authentication inside webview, let it pass
if (pos != 0) {
return;
}
// user accepted OAuth permissions - close the webview and exchange auth
// code to long lived credential.
// Also, de-register previously registered error callbacks since calling
// StopOverlay() might cause this connection to be closed.
auth_ui_context_.set_error_handler([](zx_status_t status) {});
auth_ui_context_->StopOverlay();
auto code = uri.substr(prefix.size(), std::string::npos);
// There is a '#' character at the end.
code.pop_back();
auto request =
OAuthRequestBuilder(kSpotifyOAuthTokenEndpoint, "POST")
.SetUrlEncodedBody("code=" + code + "&redirect_uri=" + kRedirectUri +
"&client_id=" + "TODO" +
"&grant_type=authorization_code");
auto request_factory = fxl::MakeCopyable(
[request = std::move(request)] { return request.Build(); });
// Generate long lived credentials (OAuth refresh token)
Request(std::move(request_factory), [this](http::URLResponse response) {
auto oauth_response = ParseOAuthResponse(std::move(response));
if (oauth_response.status != AuthProviderStatus::OK) {
FXL_VLOG(1) << "Got error: " << oauth_response.error_description;
FXL_VLOG(1) << "Got response: "
<< JsonValueToPrettyString(oauth_response.json_response);
get_persistent_credential_callback_(oauth_response.status, nullptr,
nullptr);
return;
}
if (!oauth_response.json_response.HasMember("refresh_token") ||
(!oauth_response.json_response.HasMember("access_token"))) {
FXL_VLOG(1) << "Got response: "
<< JsonValueToPrettyString(oauth_response.json_response);
get_persistent_credential_callback_(
AuthProviderStatus::OAUTH_SERVER_ERROR, nullptr, nullptr);
}
GetUserProfile(oauth_response.json_response["refresh_token"].GetString(),
oauth_response.json_response["access_token"].GetString());
});
}
void SpotifyAuthProviderImpl::GetUserProfile(
const fidl::StringPtr credential, const fidl::StringPtr access_token) {
FXL_DCHECK(credential.get().size() > 0);
FXL_DCHECK(access_token.get().size() > 0);
auto request = OAuthRequestBuilder(kSpotifyPeopleGetEndpoint, "GET")
.SetAuthorizationHeader(access_token.get());
auto request_factory = fxl::MakeCopyable(
[request = std::move(request)] { return request.Build(); });
Request(std::move(request_factory), [this,
credential](http::URLResponse response) {
fuchsia::auth::UserProfileInfoPtr user_profile_info =
fuchsia::auth::UserProfileInfo::New();
auto oauth_response = ParseOAuthResponse(std::move(response));
if (oauth_response.status != AuthProviderStatus::OK) {
FXL_VLOG(1) << "Got error: " << oauth_response.error_description;
FXL_VLOG(1) << "Got response: "
<< JsonValueToPrettyString(oauth_response.json_response);
get_persistent_credential_callback_(oauth_response.status, credential,
std::move(user_profile_info));
return;
}
if (oauth_response.json_response.HasMember("id")) {
user_profile_info->id = oauth_response.json_response["id"].GetString();
}
if (oauth_response.json_response.HasMember("displayName")) {
user_profile_info->display_name =
oauth_response.json_response["displayName"].GetString();
}
if (oauth_response.json_response.HasMember("url")) {
user_profile_info->url = oauth_response.json_response["url"].GetString();
}
if (oauth_response.json_response.HasMember("image")) {
user_profile_info->image_url =
oauth_response.json_response["image"]["url"].GetString();
}
get_persistent_credential_callback_(oauth_response.status, credential,
std::move(user_profile_info));
});
}
zx::eventpair SpotifyAuthProviderImpl::SetupWebView() {
component::Services web_view_services;
fuchsia::sys::LaunchInfo web_view_launch_info;
web_view_launch_info.url = kWebViewUrl;
web_view_launch_info.directory_request = web_view_services.NewRequest();
context_->launcher()->CreateComponent(std::move(web_view_launch_info),
web_view_controller_.NewRequest());
web_view_controller_.set_error_handler([this](zx_status_t status) {
FXL_CHECK(false) << "web_view not found at " << kWebViewUrl << ".";
});
zx::eventpair view_token, view_holder_token;
if (zx::eventpair::create(0u, &view_token, &view_holder_token) != ZX_OK)
FXL_NOTREACHED() << "Failed to create view tokens";
fuchsia::ui::app::ViewProviderPtr view_provider;
fuchsia::sys::ServiceProviderPtr incoming_view_services;
web_view_services.ConnectToService(view_provider.NewRequest());
view_provider->CreateView(std::move(view_token),
incoming_view_services.NewRequest(), nullptr);
component::ConnectToService(incoming_view_services.get(),
web_view_.NewRequest());
return view_holder_token;
}
void SpotifyAuthProviderImpl::Request(
fit::function<http::URLRequest()> request_factory,
fit::function<void(http::URLResponse response)> callback) {
requests_.emplace(network_wrapper_->Request(std::move(request_factory),
std::move(callback)));
}
} // namespace spotify_auth_provider