iTranslated by AI
Integrating LINE Login with Firebase Authentication Using Custom Tokens
Integrating Firebase Authentication and LINE Login with Custom Tokens
This article is the presentation material for the 2023-07-07 PORT Firebase meetup.
I will explain the integration of Firebase Authentication and LINE Login using custom tokens, along with sample code.
About Firebase Authentication Custom Token Authentication
First, here is the official documentation for Firebase Authentication custom tokens. Please check here for details.
Firebase Authentication natively supports various authentication providers. Below are some examples. You can check the full list from Authentication > Sign-in method in the Firebase console.
- Email/Password
- Phone
- Anonymous
- Apple
Firebase Authentication's custom token authentication can be used to integrate Firebase Authentication with authentication providers that are not natively supported. For example, this applies when using your own authentication system or LINE Login, which we are covering this time.
The basic usage is as follows:
- 1st argument (required): An ID that can uniquely identify the user or device being authenticated.
- 2nd argument (optional): Additional custom claims.
import * as admin from 'firebase-admin'
const uid = 'some-uid'
const additionalClaims = { premiumAccount: true };
const createCustomToken = async (uid: string): Promise<void> => {
const customToken = await admin.auth().createCustomToken(uid, additionalClaims)
}
Therefore, when integrating with LINE Login, it is recommended to specify the LINE user ID as the uid.
About LINE Login
Here is the official documentation for the LINE Login API reference. Please check here for details.
There is also an official documentation for the security checklist when implementing an app that incorporates LINE Login, so please be sure to check it.
Each LINE API required for integrating Firebase Authentication and LINE Login will be explained in the "Implementation Strategy" chapter.
Also, to use LINE Login, you need to configure the provider and LINE Login channel from the LINE Developers console.
The necessary configuration details are written in the following official article, "Getting Started with LINE Login."
Below, we will proceed on the assumption that the LINE Login channel settings have been completed on the LINE Developers console.
Implementation Strategy
The implementation strategy and process flow are as follows:
- Log in with LINE on the client app and obtain an access token.
- Send the access token to the backend server (we will use Firebase Functions' onCall this time).
- Verify the access token on the backend server.
- Using the verified access token, obtain LINE profile information (including the LINE user ID).
- Create a custom token using the obtained LINE user ID and return it to the client app.
- Log in on the client app using the custom token.
As mentioned in the following document, "Build a secure login process between your app and server," please note that only the access token, and not the user ID, should be sent from the client to the backend server.
Examples of processes with vulnerabilities are illustrated in the official documentation.

The secure method is shown in the figure below.

We will proceed with the implementation by referring to the aforementioned security checklist and following the secure method shown in the figure above.
Implementation
Client-side Implementation
In this example, the client app will be a Flutter app, and I will keep the explanation brief.
To implement LINE Login in a Flutter app, we will use the officially released package flutter_line_sdk. Please check the package's README for detailed setup instructions.
We will also use the cloud_functions package to call Firebase Functions on the backend from the Flutter app.
By including the following in your entry point using the LINE channel ID you created in advance, you can use the LINE SDK in your Flutter app.
void main() {
WidgetsFlutterBinding.ensureInitialized();
LineSDK.instance.setup('YOUR-CHANNEL-ID-HERE').then((_) {
print("LineSDK Prepared");
});
runApp(App());
}
The process for performing LINE Login using the LINE SDK, sending the obtained access token to the Firebase Functions backend server, receiving a custom token as a response, and using it to sign in to Firebase Authentication is as follows:
Future<void> signInWithLINE() async {
// Call the login method of LineSDK.
final loginResult = await LineSDK.instance.login();
// The obtained LoginResult type value contains the access token string.
final accessToken = loginResult.accessToken.data['access_token'] as String;
// Communicate with the backend server using httpsCallable of Firebase Functions.
// Provide the access token obtained above in the request body.
final callable = FirebaseFunctions.instanceFor(region: 'asia-northeast1')
.httpsCallable('createfirebaseauthcustomtoken');
final response = await callable.call<Map<String, dynamic>>(
<String, dynamic>{'accessToken': accessToken},
);
// Obtain the custom token created on the backend server.
final customToken = response.data['customToken'] as String;
// Sign in to Firebase Authentication using the custom token.
await FirebaseAuth.instance.signInWithCustomToken(customToken);
}
Backend Server Implementation
Next, we will implement the backend server.
Since it recently reached General Availability (GA), I decided to use the 2nd gen firebase-functions.
A Callable function named createfirebaseauthcustomtoken is defined using functions.https.onCall (Note: In 2nd gen Firebase Functions, function names must be all lowercase).
The details are also documented in the JSDoc, following the "Process Flow" explained earlier, but to summarize again:
-
verifyAccessToken: Verify the access token sent from the client. -
getLINEProfile: Using the verified access token, obtain the profile information (including the user ID) of the corresponding LINE user. - Create a custom token using the obtained
lineUserId. - (Optional) Create a user document in Cloud Firestore based on the obtained profile information.
- Return the custom token to the client.
In the sample, axios is used as the HTTP client, but fetchAPI or any other client is also fine.
import * as admin from 'firebase-admin'
import axios from 'axios'
import * as functions from 'firebase-functions/v2'
/**
* A Firebase Functions HTTPS Callable Function that verifies a LINE access token,
* retrieves LINE profile information, generates a Firebase Auth custom token,
* and sets up a user document.
* @param {Object} callableRequest - The request object provided by Firebase Functions.
* @param {string} callableRequest.data.accessToken - The LINE access token provided by the user.
* @returns {Promise<{customToken: string}>} An object containing the generated Firebase Auth custom token.
* @throws {Error} Throws an error if LINE access token verification fails, if retrieving LINE profile information fails,
* if custom token generation fails, or if setting up the user document fails.
*/
export const createfirebaseauthcustomtoken = functions.https.onCall<{ accessToken: string }>(
async (callableRequest) => {
const accessToken = callableRequest.data.accessToken
await verifyAccessToken(accessToken)
const { lineUserId, name, imageUrl } = await getLINEProfile(accessToken)
const customToken = await admin.auth().createCustomToken(lineUserId)
await setAppUserDocument({ lineUserId, name, imageUrl })
return { customToken }
}
)
Let's go through each of the functions called inside.
Verifying the Access Token
Verify the access token sent from the client using LINE's GET verify API as shown below.
In the response data from LINE's GET verify API, we check that:
-
client_id: The LINE Login channel ID is correct (matchesprocess.env.LINE_CHANNEL_ID). -
expires_in: The access token has not expired.
/**
* Calls LINE's Verify API to check the validity of the access token.
* @param {string} accessToken - The LINE access token to verify.
* @throws {Error} Throws an error if the API response status is not 200, if the LINE channel ID is incorrect,
* or if the access token has expired.
* @returns {Promise<void>} A Promise that resolves when the access token is confirmed to be valid.
*/
const verifyAccessToken = async (accessToken: string): Promise<void> => {
const response = await axios.get<LINEGetVerifyAPIResponse>(
`https://api.line.me/oauth2/v2.1/verify?access_token=${accessToken}`
)
if (response.status !== 200) {
throw new Error(`[${response.status}]: GET /oauth2/v2.1/verify`)
}
const channelId = response.data.client_id
if (channelId !== process.env.LINE_CHANNEL_ID) {
throw new Error(`The LINE Login channel ID is incorrect.`)
}
const expiresIn = response.data.expires_in
if (expiresIn <= 0) {
throw new Error(`The access token has expired.`)
}
}
Retrieving LINE User Profile Information (Including User ID)
Using LINE's profile API, retrieve the corresponding user's profile information from the verified access token.
/**
* Retrieves LINE profile information.
* @param {string} accessToken - The LINE access token.
* @returns {Promise<{ lineUserId: string; name: string; imageUrl?: string }>} A Promise that returns an object containing the user's LINE ID, name, and image URL (if it exists).
* @throws Throws an Error object containing an error message if an error occurs.
*/
const getLINEProfile = async (
accessToken: string
): Promise<{ lineUserId: string; name: string; imageUrl?: string }> => {
const response = await axios.get<LINEGetProfileResponse>(`https://api.line.me/v2/profile`, {
headers: { Authorization: `Bearer ${accessToken}` }
})
if (response.status !== 200) {
throw new Error(`[${response.status}]: GET /v2/profile`)
}
return {
lineUserId: response.data.userId,
name: response.data.displayName,
imageUrl: response.data.pictureUrl
}
}
(Optional) Creating a User Document in Cloud Firestore
Create a user document in Cloud Firestore based on the obtained profile information.
/**
* Creates or updates a 'users' document in Firestore using LINE user information.
* @param {Object} params - User information parameters.
* @param {string} params.lineUserId - The LINE user ID.
* @param {string} params.name - The LINE user name.
* @param {string} [params.imageUrl] - The URL of the LINE user image. If not provided, it sets null.
* @returns {Promise<void>} A Promise that resolves after the create or update operation is completed.
*/
const setAppUserDocument = async ({
lineUserId,
name,
imageUrl
}: {
lineUserId: string
name: string
imageUrl?: string
}): Promise<void> => {
await admin
.firestore()
.collection(`users`)
.doc(lineUserId)
.set({ name: name, imageUrl: imageUrl ?? null })
}
Conclusion
In this article, I explained how to integrate Firebase Authentication and LINE Login using custom tokens while following the official LINE Login documentation.
I showed an example of implementing the client app with Flutter and the backend server with Firebase Functions' Callable functions.
As a future outlook, although the backend server implementation is not that complex, I would like to learn how to implement and publish Firebase Extensions so that I can release this as a Firebase Extension.
The repository for the sample code can be found here:
Discussion