The subject is complicated, but you will learn a lot, and there's a big debugging section at the bottom which will help you fix any errors you have! ~~
This guide is specific for those developing apps with Firebase (Google Cloud also works directly, but I will continue to refer to Firebase) and having Firebase handle authentication.
While Naver and Firebase both support SAML (which should make the integration as easy as Kakao, Flutterfire (the Flutter SDK for Firebase) does not support SAML for Android/iOS apps - So we have to do a lot of the work manually.
While other guides for Flutter + Firebase + Naver exist, they're hard to follow, and sometimes insecure! This guide will go over absolutely everything.
https://your-firebase-project-id.firebaseapp.com
https://us-central1-your-firebase-project-id.cloudfunctions.net/naverLoginCallback
You can now find your Client ID and Client Secret in the 개요 tab of your Application.
Future<void> signInWithNaver() async {
final String clientId = 'yourNaverClientId';
final String redirectUri = 'https://uscentral-1-your-firebase-project-id.cloudfunctions.net/naverLoginCallback'; // We will create this on Google Cloud
final String state = base64Url.encode(List<int>.generate(16, (_) => Random().nextInt(255))); // Random nonce used for security
final Uri url = Uri.parse('https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id=$clientId&redirect_uri=$redirectUri&state=$state');
print("Opening Naver SSO (Naver will call our Cloud Function which will use Callback Scheme to re-enter app..");
await launchUrl(url); // flutter pub get url_launcher
}
StreamSubscription? _sub;
// This is where our app will start up again after the Cloud Function runs
Future<void> initUniLinks() async {
final initialLink = await getInitialLink(); // flutter pub get uni_links
// Handle the initial link if it exists
if (initialLink != null) _handleDeepLink(initialLink);
// This will handle the link that the app receives while already opened
linkStream.listen((String? link) {
_handleDeepLink(link!);
}, onError: (err, stacktrace) {
print("Error while listening to deep links: $err\n$stacktrace");
});
}
Future<void> _handleDeepLink(String link) async {
print("Handling deep link $link");
final Uri uri = Uri.parse(link);
if (uri.authority == 'login-callback') {
final firebaseToken = uri.queryParameters['firebaseToken'];
final String? name = uri.queryParameters['name'];
final String? profileImage = uri.queryParameters['profileImage'];
UserCredential userCredential = await FirebaseAuth.instance.signInWithCustomToken(firebaseToken!);
print('Signed in user ${userCredential.user}');
userCredential.user!.updateDisplayName(name);
userCredential.user!.updatePhotoURL(profileImage);
}
}
// This can be changed to whatever your first screen's widget is
class YourMainClass extends StatefulWidget {
const YourMainClass({Key? key}) : super(key: key);
_YourMainClassState createState() => _YourMainClassState();
}
// Again, use whatever your main Widget is here
class _YourMainClassState extends State<YourMainClass> {
void initState() {
super.initState();
print("Initing Uni Links - it's now possible for the app to know the yournaverauthcallbackscheme was used!");
initUniLinks();
}
void dispose() {
_sub?.cancel();
super.dispose();
}
Widget build(BuildContext context) {
return ElevatedButton(
child: Text("Naver Login"),
onPressed: () async {
await signInWithNaver();
});
}
}
Every app needs a unique string that browsers can call to open it up. Replace yournaverauthcallbackscheme
in the following section with something related to your app (in my app, 직음이직, I use nownownaver
).
Add the string in the following 2 places:
AndroidManifest.xml: manifest > application > activity:
<activity android:name=".MainActivity">
<!-- ... other configurations ... -->
<!-- Add the following intent-filter within the <activity> tag -->
<!-- Copy this whole <intent-filter> below any other <intent-filter>'s you already have! -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Must be lower case! -->
<data android:scheme="yournaverauthcallbackscheme" />
</intent-filter>
</activity>
info.plist: under CFBundleURLTypes > array/dict > CFBundleURLSchemes > array:
<key>CFBundleURLTypes</key>
<array>
<dict>
<!-- Other keys -->
<key>CFBundleURLSchemes</key>
<array>
<!-- Add this <string> with your custom lowercase scheme here! -->
<string>yournaverauthcallbackscheme</string>
</array>
</dict>
</array>
Fun fact! If you do a fresh app compile, you will be able to visit the URI yournaverauthcallbackscheme://random and it will open up your app! (Again, remember to replace every yournaverauthcallbackscheme
with the string you made up)
The Cloud Function is the secret ingrediant to making this work. It's as easy as it sounds - We'll write some code, and be able to run it on "the cloud" without worrying about servers (and it's much more secure).
Because the Naver docs have an example with NodeJS, I'm going to use NodeJS for the Cloud Function.
naverLoginCallback
us-central1
Runtime, build connections and security settings
> Set the Service Account to "firebase-adminsdk"main
(We'll write the "main" function next)Change package.json
to
{
"dependencies": {
"@google-cloud/functions-framework": "^3.0.0",
"firebase-admin": "^9.12.0",
"cors": "^2.8.5",
"node-fetch": "^2.6.7"
}
}
And change index.js
to
const functions = require('@google-cloud/functions-framework');
const admin = require('firebase-admin');
const cors = require('cors')({ origin: true });
const fetch = require('node-fetch');
admin.initializeApp();
// This function will be called every time someone logs in
functions.http('main', (req, res) => {
console.log("Successfully hit the cloud function..");
cors(req, res, async () => {
const { code, state } = req.query;
// CHANGE THESE!!!
const client_id = your=client-id;
const client_secret = your-client-secret; // If you want to be extra secure, put this in a Google Cloud Secret
const redirect_uri = 'yournaverauthcallbackscheme://login-callback';
try {
const accessToken = await fetchNaverToken(code, state, client_id, client_secret, redirect_uri); // A token that lets us ask Naver about the user's details
const naverProfile = await fetchNaverUserProfile(accessToken);
const firebaseToken = await createFirebaseToken(naverProfile); // JWT token that says we authenticated the user, so Firebase can trust anyone who has it
// This is what opens your app back up
res.redirect(`${redirect_uri}?firebaseToken=${firebaseToken}&name=${encodeURIComponent(naverProfile.name)}&profileImage=${encodeURIComponent(naverProfile.profile_image)}`);
} catch (error) {
console.error('Error processing the authentication:', error);
res.status(500).send(error.message);
}
});
});
async function fetchNaverToken(code, state, client_id, client_secret, redirect_uri) {
const tokenUrl = `https://nid.naver.com/oauth2.0/token?grant_type=authorization_code&client_id=${client_id}&client_secret=${client_secret}&redirect_uri=${redirect_uri}&code=${code}&state=${state}`;
const response = await fetch(tokenUrl);
const data = await response.json();
if (!response.ok) {
throw new Error(`Error from Naver: ${response.status} ${response.statusText}`);
}
if (data.error) {
throw new Error(`Error from Naver: ${data.error} ${data.error_description}`);
}
return data.access_token;
}
async function fetchNaverUserProfile(accessToken) {
const profileUrl = 'https://openapi.naver.com/v1/nid/me';
const response = await fetch(profileUrl, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
const data = await response.json();
if (!response.ok) {
throw new Error(`Error fetching Naver user profile: ${response.status} ${response.statusText}`);
}
if (data.error) {
throw new Error(`Error from Naver: ${data.error} ${data.error_description}`);
}
console.log(`Got the following data from Naver: ${JSON.stringify(data.response)}`);
return data.response;
}
async function createFirebaseToken(naverProfile) {
const uid = naverProfile.id;
try {
await admin.auth().getUser(uid);
} catch (error) {
// If the user does not exist, create them
if (error.code === 'auth/user-not-found') {
await admin.auth().createUser({
uid: uid,
displayName: naverProfile.name,
photoURL: naverProfile.profile_image,
email: naverProfile.email,
emailVerified: true
});
} else {
// If another error occurred, throw it
throw error;
}
}
// Create a JWT token that the user can use to prove they logged in
return admin.auth().createCustomToken(uid);
}
yournaverauthcallbackscheme
in the code to your custom Naver auth callback scheme.client_id
to your Client ID under 애플리케이션 정보 on Naveryour-client-secret;
to your Client Secret under 애플리케이션 정보 on NaverClick Deploy, and wait for all the spinners to stop.
The login process is quite simple:
At each stage (1 -> 2 -> 3), different information is passed:
FirebaseAuth.instance.signInWithCustomToken
Tips:
Sign in with Naver says 페이지를 찾을 수 없습니다
signInWithNaver()
has the correct clientId
Sign in with Naver says Unable to log in to <your app>
or You cannot log in with your NAVER account due to an error in the <your app> service. If the same problem persists, please contact the <your app> admin.
signInWithNaver()
has the correct redirectUri
(the Cloud Function URL we made in step 4) redirectUri
App doesn't open again after logging into Naver (App has a blank page after logging in to Naver)
App says Error: Forbidden
or Your client does not have permission to get URL /naverLoginCallback?code=... from this server
after logging in with Naver
App says "Internal Server Error" after signing in
App is blank page after signing in
App doesn't do anything after opening up again
initUniLinks()
Getting Error: Page not found
after signing in
IAM Service Account Credentials API has not been used in project ##### before or it is disabled.
Permission 'iam.serviceAccounts.signBlob' denied on resource (or it may not exist).