Skip to content

Commit

Permalink
feat(auth): new authentication flow using cookies and webview in android
Browse files Browse the repository at this point in the history
  • Loading branch information
KRTirtho committed Oct 3, 2022
1 parent 139d4dc commit 756b910
Show file tree
Hide file tree
Showing 21 changed files with 275 additions and 239 deletions.
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ buildscript {
}

dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath 'com.android.tools.build:gradle:7.2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
Expand Down
2 changes: 1 addition & 1 deletion android/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip
Binary file modified assets/tutorial/step-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/tutorial/step-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/tutorial/step-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed assets/tutorial/step-3a.jpg
Binary file not shown.
Binary file removed assets/tutorial/step-3b.jpg
Binary file not shown.
Binary file removed assets/tutorial/step-4.jpg
Binary file not shown.
13 changes: 0 additions & 13 deletions lib/components/Home/Home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,6 @@ import 'package:spotube/provider/SpotifyRequests.dart';
import 'package:spotube/provider/UserPreferences.dart';
import 'package:spotube/utils/platform.dart';

List<String> spotifyScopes = [
"playlist-modify-public",
"playlist-modify-private",
"playlist-read-private",
"user-library-read",
"user-library-modify",
"user-read-private",
"user-read-email",
"user-follow-read",
"user-follow-modify",
"playlist-read-collaborative"
];

final selectedIndexState = StateProvider((ref) => 0);

class Home extends HookConsumerWidget {
Expand Down
102 changes: 16 additions & 86 deletions lib/components/Login/LoginTutorial.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:introduction_screen/introduction_screen.dart';
import 'package:spotube/components/Login/LoginForm.dart';
import 'package:spotube/components/Login/TokenLoginForms.dart';
import 'package:spotube/components/Shared/Hyperlink.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/provider/Auth.dart';
Expand All @@ -30,12 +28,12 @@ class LoginTutorial extends ConsumerWidget {
back: const Text("Previous"),
showBackButton: true,
overrideDone: TextButton(
child: const Text("Done"),
onPressed: auth.isLoggedIn
? () {
GoRouter.of(context).go("/");
}
: null,
child: const Text("Done"),
),
pages: [
PageViewModel(
Expand All @@ -48,12 +46,12 @@ class LoginTutorial extends ConsumerWidget {
style: Theme.of(context).textTheme.bodyText1,
),
Hyperlink(
"developer.spotify.com/dashboard ",
"https://developer.spotify.com/dashboard",
"accounts.spotify.com ",
"https://accounts.spotify.com",
style: Theme.of(context).textTheme.bodyText1!,
),
Text(
"and Login if you're not logged in",
"and Login/Sign up if you're not logged in",
style: Theme.of(context).textTheme.bodyText1,
),
],
Expand All @@ -63,89 +61,21 @@ class LoginTutorial extends ConsumerWidget {
title: "Step 2",
image: Image.asset("assets/tutorial/step-2.png"),
bodyWidget: Text(
"Now, create an Spotify Developer Application by Clicking on the \"CREATE AN APP\" button. Give it a name and description too",
"1. Once you're logged in, press F12 or Mouse Right Click > Inspect to Open the Browser devtools.\n2. Then go the \"Application\" Tab (Chrome, Edge, Brave etc..) or \"Storage\" Tab (Firefox, Palemoon etc..)\n3. Go to the \"Cookies\" section then the \"https://accounts.spotify.com\" subsection",
textAlign: TextAlign.left,
style: Theme.of(context).textTheme.bodyText1,
),
),
PageViewModel(
title: "Step 3 [Really Important!]",
bodyWidget: Column(
children: [
Text(
"Tap on the \"EDIT SETTINGS\" Button & navigate to \"Redirect URIs\" section",
textAlign: TextAlign.left,
style: Theme.of(context).textTheme.bodyText1,
),
const SizedBox(height: 10),
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Text(
"Add ",
style: Theme.of(context).textTheme.bodyText1,
),
OutlinedButton(
child: Text(
"http://localhost:4304/auth/spotify/callback",
style: Theme.of(context).textTheme.bodyText1?.copyWith(
color: Theme.of(context).primaryColor,
),
),
style: OutlinedButton.styleFrom(
shape: const RoundedRectangleBorder(),
),
onPressed: () async {
await Clipboard.setData(
const ClipboardData(
text: "http://localhost:4304/auth/spotify/callback",
),
);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
width: 300,
behavior: SnackBarBehavior.floating,
content: Text(
"Copied http://localhost:4304/auth/spotify/callback to clipboard",
textAlign: TextAlign.center,
),
),
);
},
),
Text(
" to \"Redirect URIs\"",
style: Theme.of(context).textTheme.bodyText1,
),
],
),
const SizedBox(height: 10),
Wrap(
runSpacing: 10,
spacing: 10,
children: [
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 500),
child: Image.asset(
"assets/tutorial/step-3a.jpg",
),
),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 700),
child: Image.asset(
"assets/tutorial/step-3b.jpg",
),
),
],
),
],
title: "Step 3",
image: Image.asset(
"assets/tutorial/step-3.png",
),
bodyWidget: Text(
"Copy the values of \"sp_dc\" and \"sp_key\" Cookies",
textAlign: TextAlign.left,
style: Theme.of(context).textTheme.bodyText1,
),
),
PageViewModel(
title: "Step 4",
image: Image.asset("assets/tutorial/step-4.jpg"),
body:
"Finally, reveal the \"Client Secret\" by clicking on the \"SHOW CLIENT SECRET\" text\n Copy the Client ID & Client Secret then Paste them in the next Screen",
),
if (auth.isLoggedIn)
PageViewModel(
Expand All @@ -163,11 +93,11 @@ class LoginTutorial extends ConsumerWidget {
bodyWidget: Column(
children: [
Text(
"Paste the Copied \"Client ID\" and \"Client Secret\" Here",
"Paste the copied \"sp_dc\" and \"sp_key\" values in the respective fields",
style: Theme.of(context).textTheme.bodyText1,
),
const SizedBox(height: 10),
LoginForm(),
const TokenLoginForm(),
],
),
),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/components/Login/LoginForm.dart';
import 'package:spotube/components/Login/TokenLoginForms.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/models/Logger.dart';

class Login extends HookConsumerWidget {
Login({Key? key}) : super(key: key);
final log = getLogger(Login);
class TokenLogin extends HookConsumerWidget {
const TokenLogin({Key? key}) : super(key: key);

@override
Widget build(BuildContext context, ref) {
final breakpoint = useBreakpoints();

final textTheme = Theme.of(context).textTheme;

return SafeArea(
Expand All @@ -39,8 +36,8 @@ class Login extends HookConsumerWidget {
style: Theme.of(context).textTheme.caption,
),
const SizedBox(height: 10),
LoginForm(
onDone: () => GoRouter.of(context).pop(),
TokenLoginForm(
onDone: () => GoRouter.of(context).go("/"),
),
const SizedBox(height: 10),
Wrap(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,40 +1,22 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/models/Logger.dart';
import 'package:spotube/provider/Auth.dart';
import 'package:spotube/utils/service_utils.dart';

class LoginForm extends HookConsumerWidget {
class TokenLoginForm extends HookConsumerWidget {
final void Function()? onDone;
LoginForm({this.onDone, Key? key}) : super(key: key);

final log = getLogger(LoginForm);
const TokenLoginForm({
Key? key,
this.onDone,
}) : super(key: key);

@override
Widget build(BuildContext context, ref) {
Auth authState = ref.watch(authProvider);
final clientIdController = useTextEditingController();
final clientSecretController = useTextEditingController();
final fieldError = useState(false);

Future handleLogin(Auth authState) async {
try {
if (clientIdController.value.text == "" ||
clientSecretController.value.text == "") {
fieldError.value = true;
}
await ServiceUtils.oauthLogin(
ref.read(authProvider),
clientId: clientIdController.value.text,
clientSecret: clientSecretController.value.text,
).then(
(value) => onDone?.call(),
);
} catch (e) {
log.e("[Login.handleLogin] $e");
}
}
final directCodeController = useTextEditingController();
final keyCodeController = useTextEditingController();
final mounted = useIsMounted();

return ConstrainedBox(
constraints: const BoxConstraints(
Expand All @@ -43,27 +25,27 @@ class LoginForm extends HookConsumerWidget {
child: Column(
children: [
TextField(
controller: clientIdController,
controller: directCodeController,
decoration: const InputDecoration(
hintText: "Spotify Client ID",
label: Text("ClientID"),
hintText: "Spotify \"sp_dc\" Cookie",
label: Text("sp_dc Cookie"),
),
keyboardType: TextInputType.visiblePassword,
),
const SizedBox(height: 10),
TextField(
controller: clientSecretController,
controller: keyCodeController,
decoration: const InputDecoration(
hintText: "Spotify Client Secret",
label: Text("Client Secret"),
hintText: "Spotify \"sp_key\" Cookie",
label: Text("sp_key Cookie"),
),
keyboardType: TextInputType.visiblePassword,
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () async {
if (clientSecretController.text.isEmpty ||
clientIdController.text.isEmpty) {
if (keyCodeController.text.isEmpty ||
directCodeController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Please fill in all fields"),
Expand All @@ -72,7 +54,18 @@ class LoginForm extends HookConsumerWidget {
);
return;
}
await handleLogin(authState);
final cookieHeader =
"sp_dc=${directCodeController.text}; sp_key=${keyCodeController.text}";
final body = await ServiceUtils.getAccessToken(cookieHeader);

authState.setAuthState(
accessToken: body.accessToken,
authCookie: cookieHeader,
expiration: body.expiration,
);
if (mounted()) {
onDone?.call();
}
},
child: const Text("Submit"),
)
Expand Down
70 changes: 70 additions & 0 deletions lib/components/Login/WebViewLogin.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/provider/Auth.dart';
import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/service_utils.dart';

class WebViewLogin extends HookConsumerWidget {
const WebViewLogin({Key? key}) : super(key: key);

@override
Widget build(BuildContext context, ref) {
final mounted = useIsMounted();
final auth = ref.watch(authProvider);

if (kIsDesktop) {
const Scaffold(
body: Center(
child: Text('This feature is not available on desktop'),
),
);
}

return Scaffold(
body: SafeArea(
child: InAppWebView(
initialUrlRequest: URLRequest(
url: Uri.parse("https://accounts.spotify.com/"),
),
androidOnPermissionRequest: (controller, origin, resources) async {
return PermissionRequestResponse(
resources: resources,
action: PermissionRequestResponseAction.GRANT);
},
onLoadStop: (controller, action) async {
if (action == null) return;
String url = action.toString();
if (url.endsWith("/")) {
url = url.substring(0, url.length - 1);
}

if (url == "https://accounts.spotify.com/en/status") {
final cookies =
await CookieManager.instance().getCookies(url: action);
final cookieHeader =
cookies.fold<String>("", (previousValue, element) {
if (element.name == "sp_dc" || element.name == "sp_key") {
return "$previousValue; ${element.name}=${element.value}";
}
return previousValue;
});

final body = await ServiceUtils.getAccessToken(cookieHeader);
auth.setAuthState(
accessToken: body.accessToken,
authCookie: cookieHeader,
expiration: body.expiration,
);
if (mounted()) {
GoRouter.of(context).go("/");
}
}
},
),
),
);
}
}
Loading

0 comments on commit 756b910

Please sign in to comment.