Last active
October 10, 2024 09:46
-
-
Save asika32764/ef97133109515ea22e08efc6a970d16d to your computer and use it in GitHub Desktop.
LINE OAuth Login PKCE Example (JS / TypeScript)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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