Skip to content

Instantly share code, notes, and snippets.

@asika32764
Last active October 10, 2024 09:46
Show Gist options
  • Save asika32764/ef97133109515ea22e08efc6a970d16d to your computer and use it in GitHub Desktop.
Save asika32764/ef97133109515ea22e08efc6a970d16d to your computer and use it in GitHub Desktop.
LINE OAuth Login PKCE Example (JS / TypeScript)
import axios from 'axios';
export class LineWebLogin {
scope = 'profile openid email';
authorizeUrl = 'https://access.line.me/oauth2/v2.1/authorize';
accessTokenUrl = 'https://api.line.me/oauth2/v2.1/token';
state = '';
codeVerifier = '';
constructor(public channelId: string) {
//
}
// Replace by your redirect Uri
get getRedirectUri() {
const url = new URL(location.href);
url.pathname = 'social/auth/line';
return url.toString();
}
async login(): Promise<{ accessToken: string, email: string, data: any }> {
this.state = this.generateRandomString(32);
this.codeVerifier = this.generateRandomString(64);
const challenge = await this.generateCodeChallenge(this.codeVerifier);
return new Promise((resolve, reject) => {
const loginUri = new URL(this.authorizeUrl);
loginUri.searchParams.set('client_id', this.channelId);
loginUri.searchParams.set('response_type', 'code');
loginUri.searchParams.set('scope', this.scope);
loginUri.searchParams.set('redirect_uri', this.getRedirectUri);
loginUri.searchParams.set('state', this.state);
loginUri.searchParams.set('code_challenge_method', 'S256');
loginUri.searchParams.set('code_challenge', challenge);
const loginWindow = window.open(loginUri.href, 'LineLogin', this.getWindowSizeString());
const timer = setInterval(() => {
if (loginWindow?.closed) {
clearInterval(timer);
reject('User close window');
}
}, 50);
loginWindow?.focus();
// @ts-ignore
window.completeSocialLogin = async (provider, query) => {
clearInterval(timer);
loginWindow?.close();
const code = query.code;
const state = query.state;
if (state !== this.state) {
reject('Invalid State');
}
const data = await this.getAccessToken(code);
const profile = await this.getUserProfile(data.id_token);
resolve({
accessToken: data.access_token,
email: profile.email,
data
});
};
});
}
async getAccessToken(code: string) {
const url = new URL(this.accessTokenUrl);
const form = new URLSearchParams();
form.set('grant_type', 'authorization_code');
form.set('code', code);
form.set('code_verifier', this.codeVerifier);
form.set('redirect_uri', this.getRedirectUri);
form.set('client_id', this.channelId);
const res = await axios.post(
url.toString(),
form,
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
},
);
return res.data;
}
async getUserProfile(idToken: string) {
const url = new URL('https://api.line.me/oauth2/v2.1/verify');
const form = new URLSearchParams();
form.set('client_id', this.channelId);
form.set('id_token', idToken);
const res = await axios.post(
url.toString(),
form,
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
return res.data;
}
/**
* @see https://medium.com/@steven234/%E5%9C%A8%E5%BD%88%E5%87%BA%E8%A6%96%E7%AA%97%E8%A3%A1%E7%99%BB%E5%85%A5-line-22fcb090f231
*/
getWindowSizeString() {
const w = 575;
const h = 600;
// When the user clicks on a link that opens a new window using window.open. Make the window appear on the same monitor as its' parent.
// window.screenX will give the position of the current monitor screen.
// suppose monitor width is 1360
// for monitor 1 window.screenX = 0;
// for monitor 2 window.screenX = 1360;
const dualScreenLeft =
window.screenLeft != undefined ? window.screenLeft : window.screenX;
const dualScreenTop =
window.screenTop != undefined ? window.screenTop : window.screenY;
const width = window.innerWidth
? window.innerWidth
: document.documentElement.clientWidth
? document.documentElement.clientWidth
: screen.width;
const height = window.innerHeight
? window.innerHeight
: document.documentElement.clientHeight
? document.documentElement.clientHeight
: screen.height;
// same monitor, the center of its parent.
const left = width / 2 - w / 2 + dualScreenLeft;
const top = height / 2 - h / 2 + dualScreenTop;
return `width=${w}, height=${h}, top=${top}, left=${left}`;
}
/**
* @see https://github.com/curityio/pkce-javascript-example/blob/master/README.md
* @param codeVerifier
*/
async generateCodeChallenge(codeVerifier: string) {
const digest = await crypto.subtle.digest("SHA-256",
new TextEncoder().encode(codeVerifier));
return btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
}
/**
* @see https://github.com/curityio/pkce-javascript-example/blob/master/README.md
* @param length
*/
generateRandomString(length: number) {
let text = "";
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < length; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
}
const lineWebLogin = new LineWebLogin('{Channel ID}');
const res = await lineWebLogin.login();
res.email;
res.accessToken;
res.data;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment