11const LineAdapter = require ( '../../../lib/Adapters/Auth/line' ) . default ;
2+ const jwt = require ( 'jsonwebtoken' ) ;
3+ const authUtils = require ( '../../../lib/Adapters/Auth/utils' ) ;
4+
25describe ( 'LineAdapter' , function ( ) {
36 let adapter ;
47
8+ const validOptions = {
9+ clientId : 'validClientId' ,
10+ clientSecret : 'validClientSecret' ,
11+ } ;
12+
13+ // Stub LINE JWKS lookup and JWT verification so auth-flow tests stay deterministic
14+ // and do not depend on live LINE signing keys.
15+ function mockEs256IdTokenVerification ( claims = { } ) {
16+ const jwtClaims = {
17+ iss : 'https://access.line.me' ,
18+ aud : 'validClientId' ,
19+ exp : Date . now ( ) + 1000 ,
20+ sub : 'mockUserId' ,
21+ ...claims ,
22+ } ;
23+ const getHeaderSpy = jasmine . isSpy ( authUtils . getHeaderFromToken )
24+ ? authUtils . getHeaderFromToken
25+ : spyOn ( authUtils , 'getHeaderFromToken' ) ;
26+ const getSigningKeySpy = jasmine . isSpy ( authUtils . getSigningKey )
27+ ? authUtils . getSigningKey
28+ : spyOn ( authUtils , 'getSigningKey' ) ;
29+ const verifySpy = jasmine . isSpy ( jwt . verify ) ? jwt . verify : spyOn ( jwt , 'verify' ) ;
30+ getHeaderSpy . and . returnValue ( { kid : '123' , alg : 'ES256' } ) ;
31+ getSigningKeySpy . and . resolveTo ( { publicKey : 'line_public_key' } ) ;
32+ verifySpy . and . returnValue ( jwtClaims ) ;
33+ return jwtClaims ;
34+ }
35+
536 beforeEach ( function ( ) {
637 adapter = new LineAdapter . constructor ( ) ;
738 adapter . clientId = 'validClientId' ;
839 adapter . clientSecret = 'validClientSecret' ;
940 } ) ;
1041
42+ describe ( 'validateOptions' , function ( ) {
43+ it ( 'should allow secure id token validation with clientId only' , function ( ) {
44+ expect ( ( ) => adapter . validateOptions ( { clientId : 'validClientId' } ) ) . not . toThrow ( ) ;
45+ expect ( adapter . clientId ) . toBe ( 'validClientId' ) ;
46+ expect ( adapter . clientSecret ) . toBeUndefined ( ) ;
47+ } ) ;
48+
49+ it ( 'should allow insecure-only configuration when explicitly enabled' , function ( ) {
50+ expect ( ( ) => adapter . validateOptions ( { enableInsecureAuth : true } ) ) . not . toThrow ( ) ;
51+ expect ( adapter . enableInsecureAuth ) . toBeTrue ( ) ;
52+ } ) ;
53+
54+ it ( 'should require clientId when secure auth is configured' , function ( ) {
55+ expect ( ( ) => adapter . validateOptions ( { clientSecret : 'validClientSecret' } ) ) . toThrowError (
56+ 'Line clientId is required.'
57+ ) ;
58+ } ) ;
59+ } ) ;
60+
61+ describe ( 'verifyIdToken' , function ( ) {
62+ beforeEach ( function ( ) {
63+ adapter . validateOptions ( validOptions ) ;
64+ } ) ;
65+
66+ it ( 'should throw an error if id_token is missing' , async function ( ) {
67+ await expectAsync ( adapter . verifyIdToken ( { } ) ) . toBeRejectedWithError (
68+ 'id token is invalid for this user.'
69+ ) ;
70+ } ) ;
71+
72+ it ( 'should not decode an invalid id_token' , async function ( ) {
73+ await expectAsync ( adapter . verifyIdToken ( { id_token : 'the_token' } ) ) . toBeRejectedWithError (
74+ 'provided token does not decode as JWT'
75+ ) ;
76+ } ) ;
77+
78+ it ( 'should throw an error if public key used to encode token is not available' , async function ( ) {
79+ spyOn ( authUtils , 'getHeaderFromToken' ) . and . returnValue ( { kid : '789' , alg : 'ES256' } ) ;
80+
81+ await expectAsync ( adapter . verifyIdToken ( { id_token : 'the_token' } ) ) . toBeRejectedWithError (
82+ 'Unable to find matching key for Key ID: 789'
83+ ) ;
84+ } ) ;
85+
86+ it ( 'should guard and pass only a valid supported algorithm to jwt.verify' , async function ( ) {
87+ const fakeClaim = mockEs256IdTokenVerification ( ) ;
88+
89+ const result = await adapter . verifyIdToken ( {
90+ id : 'mockUserId' ,
91+ id_token : 'the_token' ,
92+ } ) ;
93+
94+ expect ( result ) . toEqual ( fakeClaim ) ;
95+ expect ( jwt . verify . calls . first ( ) . args [ 2 ] . algorithms ) . toEqual ( [ 'ES256' ] ) ;
96+ } ) ;
97+
98+ it ( 'should verify a valid id_token without an explicit id' , async function ( ) {
99+ const fakeClaim = mockEs256IdTokenVerification ( { sub : 'line-subject' } ) ;
100+
101+ const result = await adapter . verifyIdToken ( { id_token : 'the_token' } ) ;
102+
103+ expect ( result ) . toEqual ( fakeClaim ) ;
104+ } ) ;
105+
106+ it ( 'should reject a token with an invalid issuer' , async function ( ) {
107+ mockEs256IdTokenVerification ( { iss : 'https://invalid.line.me' } ) ;
108+
109+ await expectAsync (
110+ adapter . verifyIdToken ( { id : 'mockUserId' , id_token : 'the_token' } )
111+ ) . toBeRejectedWithError (
112+ 'id token not issued by correct OpenID provider - expected: https://access.line.me | from: https://invalid.line.me'
113+ ) ;
114+ } ) ;
115+
116+ it ( 'should reject a token with a mismatched sub claim' , async function ( ) {
117+ mockEs256IdTokenVerification ( { sub : 'another-user' } ) ;
118+
119+ await expectAsync (
120+ adapter . verifyIdToken ( { id : 'mockUserId' , id_token : 'the_token' } )
121+ ) . toBeRejectedWithError ( 'auth data is invalid for this user.' ) ;
122+ } ) ;
123+
124+ it ( 'should reject a token with a mismatched nonce' , async function ( ) {
125+ mockEs256IdTokenVerification ( { nonce : 'server-nonce' } ) ;
126+
127+ await expectAsync (
128+ adapter . verifyIdToken ( {
129+ id_token : 'the_token' ,
130+ nonce : 'different-nonce' ,
131+ } )
132+ ) . toBeRejectedWithError ( 'auth data is invalid for this user.' ) ;
133+ } ) ;
134+
135+ it ( 'should verify an HS256 token when clientSecret is configured' , async function ( ) {
136+ spyOn ( authUtils , 'getHeaderFromToken' ) . and . returnValue ( { alg : 'HS256' } ) ;
137+ spyOn ( jwt , 'verify' ) . and . returnValue ( {
138+ iss : 'https://access.line.me' ,
139+ aud : 'validClientId' ,
140+ exp : Date . now ( ) + 1000 ,
141+ sub : 'mockUserId' ,
142+ } ) ;
143+
144+ const result = await adapter . verifyIdToken ( {
145+ id : 'mockUserId' ,
146+ id_token : 'the_token' ,
147+ } ) ;
148+
149+ expect ( result . sub ) . toBe ( 'mockUserId' ) ;
150+ expect ( jwt . verify . calls . first ( ) . args [ 1 ] ) . toBe ( 'validClientSecret' ) ;
151+ expect ( jwt . verify . calls . first ( ) . args [ 2 ] . algorithms ) . toEqual ( [ 'HS256' ] ) ;
152+ } ) ;
153+
154+ it ( 'should reject an HS256 token when clientSecret is missing' , async function ( ) {
155+ adapter . validateOptions ( { clientId : 'validClientId' } ) ;
156+ spyOn ( authUtils , 'getHeaderFromToken' ) . and . returnValue ( { alg : 'HS256' } ) ;
157+
158+ await expectAsync ( adapter . verifyIdToken ( { id_token : 'the_token' } ) ) . toBeRejectedWithError (
159+ 'Line clientSecret is required to verify HS256 id_token.'
160+ ) ;
161+ } ) ;
162+ } ) ;
163+
11164 describe ( 'getAccessTokenFromCode' , function ( ) {
165+ beforeEach ( function ( ) {
166+ adapter . validateOptions ( validOptions ) ;
167+ } ) ;
168+
12169 it ( 'should throw an error if code is missing in authData' , async function ( ) {
13170 const authData = { redirect_uri : 'http://example.com' } ;
14171
@@ -17,6 +174,14 @@ describe('LineAdapter', function () {
17174 ) ;
18175 } ) ;
19176
177+ it ( 'should throw an error if clientSecret is missing in server-side auth flows' , async function ( ) {
178+ adapter . validateOptions ( { clientId : 'validClientId' } ) ;
179+
180+ await expectAsync (
181+ adapter . getAccessTokenFromCode ( { code : 'validCode' , redirect_uri : 'http://example.com' } )
182+ ) . toBeRejectedWithError ( 'Line clientSecret is required to exchange code for token.' ) ;
183+ } ) ;
184+
20185 it ( 'should fetch an access token successfully' , async function ( ) {
21186 mockFetch ( [
22187 {
@@ -92,7 +257,11 @@ describe('LineAdapter', function () {
92257 } ) ;
93258
94259 describe ( 'getUserFromAccessToken' , function ( ) {
95- it ( 'should fetch user data successfully' , async function ( ) {
260+ beforeEach ( function ( ) {
261+ adapter . validateOptions ( validOptions ) ;
262+ } ) ;
263+
264+ it ( 'should fetch user data successfully and normalize the user id' , async function ( ) {
96265 mockFetch ( [
97266 {
98267 url : 'https://api.line.me/v2/profile' ,
@@ -114,6 +283,7 @@ describe('LineAdapter', function () {
114283 expect ( user ) . toEqual ( {
115284 userId : 'mockUserId' ,
116285 displayName : 'mockDisplayName' ,
286+ id : 'mockUserId' ,
117287 } ) ;
118288 } ) ;
119289
@@ -130,7 +300,7 @@ describe('LineAdapter', function () {
130300 ] ) ;
131301
132302 const accessToken = 'invalidAccessToken' ;
133-
303+
134304 await expectAsync ( adapter . getUserFromAccessToken ( accessToken ) ) . toBeRejectedWithError (
135305 'Failed to fetch Line user: Unauthorized'
136306 ) ;
@@ -156,6 +326,35 @@ describe('LineAdapter', function () {
156326 } ) ;
157327 } ) ;
158328
329+ describe ( 'beforeFind' , function ( ) {
330+ beforeEach ( function ( ) {
331+ adapter . validateOptions ( validOptions ) ;
332+ } ) ;
333+
334+ it ( 'should populate authData.id from a verified id_token' , async function ( ) {
335+ spyOn ( adapter , 'verifyIdToken' ) . and . resolveTo ( { sub : 'mockUserId' } ) ;
336+ const authData = {
337+ id_token : 'the_token' ,
338+ nonce : 'nonce' ,
339+ } ;
340+
341+ await adapter . beforeFind ( authData ) ;
342+
343+ expect ( authData ) . toEqual ( { id : 'mockUserId' } ) ;
344+ } ) ;
345+
346+ it ( 'should block insecure auth unless explicitly enabled' , async function ( ) {
347+ const authData = {
348+ id : 'mockUserId' ,
349+ access_token : 'validAccessToken' ,
350+ } ;
351+
352+ await expectAsync ( adapter . beforeFind ( authData ) ) . toBeRejectedWithError (
353+ 'Line code is required.'
354+ ) ;
355+ } ) ;
356+ } ) ;
357+
159358 describe ( 'LineAdapter E2E Test' , function ( ) {
160359 beforeEach ( async function ( ) {
161360 await reconfigureServer ( {
@@ -306,4 +505,136 @@ describe('LineAdapter', function () {
306505 } ) ;
307506 } ) ;
308507
508+ describe ( 'LineAdapter E2E id_token Test' , function ( ) {
509+ beforeEach ( async function ( ) {
510+ await reconfigureServer ( {
511+ auth : {
512+ line : {
513+ clientId : 'validClientId' ,
514+ } ,
515+ } ,
516+ } ) ;
517+ } ) ;
518+
519+ it ( 'should log in user successfully with a valid id_token' , async function ( ) {
520+ mockEs256IdTokenVerification ( ) ;
521+
522+ const authData = {
523+ id_token : 'the_token' ,
524+ } ;
525+
526+ const user = await Parse . User . logInWith ( 'line' , { authData } ) ;
527+ await user . fetch ( { useMasterKey : true } ) ;
528+
529+ expect ( user . id ) . toBeDefined ( ) ;
530+ expect ( user . get ( 'authData' ) . line . id ) . toBe ( 'mockUserId' ) ;
531+ } ) ;
532+
533+ it ( 'should link line auth to an existing logged-in user with an id_token' , async function ( ) {
534+ mockEs256IdTokenVerification ( { sub : 'link-user-id' } ) ;
535+ const user = await Parse . User . signUp ( 'line-link-user' , 'password' ) ;
536+
537+ await user . save (
538+ {
539+ authData : {
540+ line : {
541+ id_token : 'the_token' ,
542+ } ,
543+ } ,
544+ } ,
545+ { sessionToken : user . getSessionToken ( ) }
546+ ) ;
547+
548+ await user . fetch ( { useMasterKey : true } ) ;
549+ expect ( user . get ( 'authData' ) . line . id ) . toBe ( 'link-user-id' ) ;
550+ } ) ;
551+
552+ it ( 'should allow updating existing LINE auth with id_token' , async function ( ) {
553+ mockEs256IdTokenVerification ( { sub : 'existing-line-user' } ) ;
554+ const user = await Parse . User . logInWith ( 'line' , {
555+ authData : {
556+ id_token : 'first_token' ,
557+ } ,
558+ } ) ;
559+
560+ mockEs256IdTokenVerification ( { sub : 'existing-line-user' } ) ;
561+ await user . save (
562+ {
563+ authData : {
564+ line : {
565+ id_token : 'second_token' ,
566+ } ,
567+ } ,
568+ } ,
569+ { sessionToken : user . getSessionToken ( ) }
570+ ) ;
571+
572+ await user . fetch ( { useMasterKey : true } ) ;
573+ expect ( user . get ( 'authData' ) . line . id ) . toBe ( 'existing-line-user' ) ;
574+ } ) ;
575+
576+ it ( 'should reject insecure authData when insecure auth is disabled' , async function ( ) {
577+ await expectAsync (
578+ Parse . User . logInWith ( 'line' , {
579+ authData : {
580+ id : 'mockUserId' ,
581+ access_token : 'validAccessToken' ,
582+ } ,
583+ } )
584+ ) . toBeRejectedWithError ( 'Line code is required.' ) ;
585+ } ) ;
586+
587+ it ( 'should handle invalid id_token claims during login' , async function ( ) {
588+ mockEs256IdTokenVerification ( { iss : 'https://invalid.line.me' } ) ;
589+
590+ await expectAsync (
591+ Parse . User . logInWith ( 'line' , {
592+ authData : {
593+ id_token : 'the_token' ,
594+ } ,
595+ } )
596+ ) . toBeRejectedWithError (
597+ 'id token not issued by correct OpenID provider - expected: https://access.line.me | from: https://invalid.line.me'
598+ ) ;
599+ } ) ;
600+ } ) ;
601+
602+ describe ( 'LineAdapter E2E legacy access token Test' , function ( ) {
603+ beforeEach ( async function ( ) {
604+ await reconfigureServer ( {
605+ auth : {
606+ line : {
607+ enableInsecureAuth : true ,
608+ } ,
609+ } ,
610+ } ) ;
611+ } ) ;
612+
613+ it ( 'should allow insecure auth only when explicitly enabled' , async function ( ) {
614+ mockFetch ( [
615+ {
616+ url : 'https://api.line.me/v2/profile' ,
617+ method : 'GET' ,
618+ response : {
619+ ok : true ,
620+ json : ( ) =>
621+ Promise . resolve ( {
622+ userId : 'mockUserId' ,
623+ displayName : 'mockDisplayName' ,
624+ } ) ,
625+ } ,
626+ } ,
627+ ] ) ;
628+
629+ const user = await Parse . User . logInWith ( 'line' , {
630+ authData : {
631+ id : 'mockUserId' ,
632+ access_token : 'validAccessToken' ,
633+ } ,
634+ } ) ;
635+
636+ expect ( user . id ) . toBeDefined ( ) ;
637+ expect ( user . get ( 'authData' ) . line . id ) . toBe ( 'mockUserId' ) ;
638+ } ) ;
639+ } ) ;
309640} ) ;
0 commit comments