-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(authentication): add a middleware for authentication
- Loading branch information
1 parent
f793b74
commit b72ba01
Showing
7 changed files
with
844 additions
and
14 deletions.
There are no files selected for viewing
272 changes: 272 additions & 0 deletions
272
...ges/authentication/src/__tests__/acceptance/basic-auth-extension.middleware.acceptance.ts
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,272 @@ | ||
// Copyright IBM Corp. 2019,2020. All Rights Reserved. | ||
// Node module: @loopback/authentication | ||
// This file is licensed under the MIT License. | ||
// License text available at https://opensource.org/licenses/MIT | ||
|
||
import {Application, inject} from '@loopback/core'; | ||
import {anOpenApiSpec} from '@loopback/openapi-spec-builder'; | ||
import {api, get} from '@loopback/openapi-v3'; | ||
import {Request, RestServer} from '@loopback/rest'; | ||
import {SecurityBindings, securityId, UserProfile} from '@loopback/security'; | ||
import {Client, createClientForHandler, expect} from '@loopback/testlab'; | ||
import { | ||
authenticate, | ||
AuthenticationBindings, | ||
registerAuthenticationStrategy, | ||
} from '../..'; | ||
import {AuthenticationStrategy} from '../../types'; | ||
import { | ||
createBasicAuthorizationHeaderValue, | ||
getApp, | ||
getUserRepository, | ||
myUserProfileFactory, | ||
} from '../fixtures/helper'; | ||
import {BasicAuthenticationStrategyBindings, USER_REPO} from '../fixtures/keys'; | ||
import {AuthenticationMiddlewareSequence} from '../fixtures/sequences/authentication.middleware.sequence'; | ||
import {BasicAuthenticationUserService} from '../fixtures/services/basic-auth-user-service'; | ||
import {BasicAuthenticationStrategy} from '../fixtures/strategies/basic-strategy'; | ||
import {User} from '../fixtures/users/user'; | ||
import {UserRepository} from '../fixtures/users/user.repository'; | ||
|
||
describe('Basic Authentication', () => { | ||
let app: Application; | ||
let server: RestServer; | ||
let users: UserRepository; | ||
let joeUser: User; | ||
beforeEach(givenAServer); | ||
beforeEach(givenControllerInApp); | ||
beforeEach(givenAuthenticatedSequence); | ||
beforeEach(givenProviders); | ||
|
||
it(`authenticates successfully for correct credentials of user 'jack'`, async () => { | ||
const client = whenIMakeRequestTo(server); | ||
await client | ||
.get('/whoAmI') | ||
.set('Authorization', createBasicAuthorizationHeaderValue(joeUser)) | ||
.expect(joeUser.id); | ||
}); | ||
|
||
it('returns error for missing Authorization header', async () => { | ||
const client = whenIMakeRequestTo(server); | ||
|
||
await client.get('/whoAmI').expect({ | ||
error: { | ||
message: 'Authorization header not found.', | ||
name: 'UnauthorizedError', | ||
statusCode: 401, | ||
}, | ||
}); | ||
}); | ||
|
||
it(`returns error for missing 'Basic ' portion of Authorization header value`, async () => { | ||
const client = whenIMakeRequestTo(server); | ||
await client | ||
.get('/whoAmI') | ||
.set( | ||
'Authorization', | ||
createBasicAuthorizationHeaderValue(joeUser, {prefix: 'NotB@sic '}), | ||
) | ||
.expect({ | ||
error: { | ||
message: `Authorization header is not of type 'Basic'.`, | ||
name: 'UnauthorizedError', | ||
statusCode: 401, | ||
}, | ||
}); | ||
}); | ||
|
||
it(`returns error for too many parts in Authorization header value`, async () => { | ||
const client = whenIMakeRequestTo(server); | ||
await client | ||
.get('/whoAmI') | ||
.set( | ||
'Authorization', | ||
createBasicAuthorizationHeaderValue(joeUser) + ' someOtherValue', | ||
) | ||
.expect({ | ||
error: { | ||
message: `Authorization header value has too many parts. It must follow the pattern: 'Basic xxyyzz' where xxyyzz is a base64 string.`, | ||
name: 'UnauthorizedError', | ||
statusCode: 401, | ||
}, | ||
}); | ||
}); | ||
|
||
it(`returns error for missing ':' in decrypted Authorization header credentials value`, async () => { | ||
const client = whenIMakeRequestTo(server); | ||
await client | ||
.get('/whoAmI') | ||
.set( | ||
'Authorization', | ||
createBasicAuthorizationHeaderValue(joeUser, {separator: '|'}), | ||
) | ||
.expect({ | ||
error: { | ||
message: `Authorization header 'Basic' value does not contain two parts separated by ':'.`, | ||
name: 'UnauthorizedError', | ||
statusCode: 401, | ||
}, | ||
}); | ||
}); | ||
|
||
it(`returns error for too many parts in decrypted Authorization header credentials value`, async () => { | ||
const client = whenIMakeRequestTo(server); | ||
await client | ||
.get('/whoAmI') | ||
.set( | ||
'Authorization', | ||
createBasicAuthorizationHeaderValue(joeUser, { | ||
extraSegment: 'extraPart', | ||
}), | ||
) | ||
.expect({ | ||
error: { | ||
message: `Authorization header 'Basic' value does not contain two parts separated by ':'.`, | ||
name: 'UnauthorizedError', | ||
statusCode: 401, | ||
}, | ||
}); | ||
}); | ||
|
||
it('allows anonymous requests to methods with no decorator', async () => { | ||
class InfoController { | ||
@get('/status') | ||
status() { | ||
return {running: true}; | ||
} | ||
} | ||
|
||
app.controller(InfoController); | ||
await whenIMakeRequestTo(server) | ||
.get('/status') | ||
.expect(200, {running: true}); | ||
}); | ||
|
||
it('returns error for unknown authentication strategy', async () => { | ||
class InfoController { | ||
@get('/status') | ||
@authenticate('doesnotexist') | ||
status() { | ||
return {running: true}; | ||
} | ||
} | ||
|
||
app.controller(InfoController); | ||
await whenIMakeRequestTo(server) | ||
.get('/status') | ||
.expect({ | ||
error: { | ||
message: `The strategy 'doesnotexist' is not available.`, | ||
name: 'Error', | ||
statusCode: 401, | ||
code: 'AUTHENTICATION_STRATEGY_NOT_FOUND', | ||
}, | ||
}); | ||
}); | ||
|
||
it('returns error when undefined user profile returned from authentication strategy', async () => { | ||
class BadBasicStrategy implements AuthenticationStrategy { | ||
name = 'badbasic'; | ||
async authenticate(request: Request): Promise<UserProfile | undefined> { | ||
return undefined; | ||
} | ||
} | ||
registerAuthenticationStrategy(server, BadBasicStrategy); | ||
|
||
class InfoController { | ||
@get('/status') | ||
@authenticate('badbasic') | ||
status() { | ||
return {running: true}; | ||
} | ||
} | ||
|
||
app.controller(InfoController); | ||
await whenIMakeRequestTo(server) | ||
.get('/status') | ||
.expect({ | ||
error: { | ||
message: `User profile not returned from strategy's authenticate function`, | ||
name: 'Error', | ||
statusCode: 401, | ||
code: 'USER_PROFILE_NOT_FOUND', | ||
}, | ||
}); | ||
}); | ||
|
||
it('adds security scheme component to apiSpec', async () => { | ||
const EXPECTED_SPEC = { | ||
components: { | ||
securitySchemes: { | ||
basic: { | ||
type: 'http', | ||
scheme: 'basic', | ||
}, | ||
}, | ||
}, | ||
}; | ||
const spec = await server.getApiSpec(); | ||
expect(spec).to.containDeep(EXPECTED_SPEC); | ||
}); | ||
|
||
async function givenAServer() { | ||
app = getApp(); | ||
server = await app.getServer(RestServer); | ||
} | ||
|
||
function givenControllerInApp() { | ||
const apispec = anOpenApiSpec() | ||
.withOperation('get', '/whoAmI', { | ||
'x-operation-name': 'whoAmI', | ||
responses: { | ||
'200': { | ||
description: '', | ||
schema: { | ||
type: 'string', | ||
}, | ||
}, | ||
}, | ||
}) | ||
.build(); | ||
|
||
@api(apispec) | ||
class MyController { | ||
constructor() {} | ||
|
||
@authenticate('basic') | ||
async whoAmI( | ||
@inject(SecurityBindings.USER) userProfile: UserProfile, | ||
): Promise<string> { | ||
if (!userProfile) return 'userProfile is undefined'; | ||
if (!userProfile[securityId]) return 'userProfile id is undefined'; | ||
return userProfile[securityId]; | ||
} | ||
} | ||
app.controller(MyController); | ||
} | ||
|
||
function givenAuthenticatedSequence() { | ||
// bind user defined sequence | ||
server.sequence(AuthenticationMiddlewareSequence); | ||
} | ||
|
||
function givenProviders() { | ||
registerAuthenticationStrategy(server, BasicAuthenticationStrategy); | ||
|
||
server | ||
.bind(BasicAuthenticationStrategyBindings.USER_SERVICE) | ||
.toClass(BasicAuthenticationUserService); | ||
|
||
users = getUserRepository(); | ||
joeUser = users.list['joe888']; | ||
server.bind(USER_REPO).to(users); | ||
|
||
server | ||
.bind(AuthenticationBindings.USER_PROFILE_FACTORY) | ||
.to(myUserProfileFactory); | ||
} | ||
|
||
function whenIMakeRequestTo(restServer: RestServer): Client { | ||
return createClientForHandler(restServer.requestHandler); | ||
} | ||
}); |
Oops, something went wrong.