Skip to content

Instantly share code, notes, and snippets.

@bseib
Created January 29, 2020 14:44
Show Gist options
  • Save bseib/da68438e3a55cede78698362e80001ad to your computer and use it in GitHub Desktop.
Save bseib/da68438e3a55cede78698362e80001ad to your computer and use it in GitHub Desktop.
loopback 4 authentication authorization example
import {BootMixin} from '@loopback/boot';
import {ApplicationConfig} from '@loopback/core';
import {
AuthenticationComponent,
registerAuthenticationStrategy,
AuthenticationBindings,
} from '@loopback/authentication';
import {
// AuthorizationOptions,
// AuthorizationDecision,
AuthorizationComponent,
AuthorizationTags,
} from '@loopback/authorization';
import {
RestExplorerBindings,
RestExplorerComponent,
} from '@loopback/rest-explorer';
import {RepositoryMixin} from '@loopback/repository';
import {RestApplication} from '@loopback/rest';
import {ServiceMixin} from '@loopback/service-proxy';
import path from 'path';
import {MySequence} from './sequence';
import {MyAuthenticationStrategy} from './MyAuthenticationStrategy';
import {MyAuthenticationStrategyBindings} from './keys';
import {MyMachineUserService} from './services';
import {MyAuthorizationProvider} from './MyAuthorizationProvider';
export class HelloLoopbackApplication extends BootMixin(
ServiceMixin(RepositoryMixin(RestApplication)),
) {
constructor(options: ApplicationConfig = {}) {
super(options);
// Set up authentication. The real work is added to MySequence.
this.component(AuthenticationComponent);
registerAuthenticationStrategy(this, MyAuthenticationStrategy);
this.bind(MyAuthenticationStrategyBindings.MACHINE_USER_SERVICE).toClass(
MyMachineUserService,
);
this.bind(MyAuthenticationStrategyBindings.DEFAULT_OPTIONS).to({
public: false,
});
// turn on authentication by default for all endpoints
// https://loopback.io/doc/en/lb4/Loopback-component-authentication.html#default-authentication-metadata
this.configure(AuthenticationBindings.COMPONENT).to({
defaultMetadata: {strategy: 'my'},
});
// setup authorization
this.component(AuthorizationComponent);
this.bind('authorizationProviders.my-authorization-provider')
.toProvider(MyAuthorizationProvider)
.tag(AuthorizationTags.AUTHORIZER);
// Set up the custom sequence
this.sequence(MySequence);
// Set up default home page
this.static('/', path.join(__dirname, '../public'));
// Customize @loopback/rest-explorer configuration here
this.bind(RestExplorerBindings.CONFIG).to({
path: '/explorer',
});
this.component(RestExplorerComponent);
this.projectRoot = __dirname;
// Customize @loopback/boot Booter Conventions here
this.bootOptions = {
controllers: {
// Customize ControllerBooter Conventions here
dirs: ['controllers'],
extensions: ['.controller.js'],
nested: true,
},
};
}
}
import {bind, BindingScope} from '@loopback/core';
import {Credentials} from '../MyAuthenticationStrategy';
import {UserProfile, securityId} from '@loopback/security';
//import {MyUserRepository} from '../repositories';
import {repository, WhereBuilder} from '@loopback/repository';
import {MyUser} from '../models';
//import {USER_PROFILE_NOT_FOUND} from '@loopback/authentication';
@bind({scope: BindingScope.TRANSIENT})
export class MyMachineUserService {
constructor(
@repository(MyUserRepository)
public myUserRepository: MyUserRepository,
) {}
async verifyCredentials(
credentials: Credentials,
): Promise<MyUser | undefined> {
if ( credentials.username === 'bob' && credentials.password === 'supersecret' ) {
return {
userId: 42,
name: 'bob',
foo: 'bar',
};
}
else {
return undefined;
}
}
convertToUserProfile(user: MyUser): UserProfile | undefined {
return {
[securityId]: '' + user.userId,
name: user.name,
user: user,
};
}
}
import {inject} from '@loopback/core';
import {
AuthenticationStrategy,
AuthenticationBindings,
AuthenticationMetadata,
} from '@loopback/authentication';
import {UserProfile} from '@loopback/security';
import {MyMachineUserService} from './services';
import {
MyAuthenticationStrategyBindings,
MyAuthenticationOptions,
} from './keys';
import {Request} from 'express';
export interface Credentials {
username: string;
password: string;
}
export class MyAuthenticationStrategy implements AuthenticationStrategy {
name = 'my';
@inject(MyAuthenticationStrategyBindings.DEFAULT_OPTIONS)
defaultOptions: MyAuthenticationOptions;
constructor(
@inject(MyAuthenticationStrategyBindings.MACHINE_USER_SERVICE)
private machineUserService: MyMachineUserService,
@inject(AuthenticationBindings.METADATA)
private authenMetadata: AuthenticationMetadata,
) {}
async authenticate(request: Request): Promise<UserProfile | undefined> {
const options = this.computeAuthenticationOptions();
const credentials = this.extractCredentials(request);
if (credentials === undefined) {
return undefined;
}
const user = await this.machineUserService.verifyCredentials(credentials);
if (user === undefined) {
return undefined;
}
const userProfile = this.machineUserService.convertToUserProfile(user);
return userProfile;
}
extractCredentials(request: Request): Credentials | undefined {
return {
username: 'bob',
password: '42',
};
}
computeAuthenticationOptions(): MyAuthenticationOptions {
/**
Obtain the options object specified in the @authenticate decorator
of a controller method associated with the current request.
The AuthenticationMetadata interface contains : strategy:string, options?:object
We want the options property.
*/
return Object.assign(
{foo: false}, // the ultimate default, if global default is empty
this.defaultOptions || {},
this.authenMetadata.options,
) as MyAuthenticationOptions;
}
}
import {Provider} from '@loopback/core';
import {
Authorizer,
AuthorizationContext,
AuthorizationMetadata,
AuthorizationDecision,
} from '@loopback/authorization';
export class MyAuthorizationProvider implements Provider<Authorizer> {
/**
* @returns an authorizer function
*
*/
value(): Authorizer {
return this.authorize.bind(this);
}
async authorize(
context: AuthorizationContext,
metadata: AuthorizationMetadata,
) {
//events.push(context.resource);
// if (
// context.resource === 'OrderController.prototype.cancelOrder' &&
// context.principals[0].name === 'user-01'
// ) {
// return AuthorizationDecision.DENY;
// }
const isPublic = !!metadata.skip;
console.log(`authz is public = ${isPublic}`);
console.log(`authorize context = `, context);
console.log(`authorize metadata = `, metadata);
return AuthorizationDecision.ALLOW;
}
}
import {inject} from '@loopback/context';
import {
FindRoute,
InvokeMethod,
ParseParams,
Reject,
RequestContext,
RestBindings,
Send,
SequenceHandler,
} from '@loopback/rest';
import {
AuthenticationBindings,
AuthenticateFn,
AUTHENTICATION_STRATEGY_NOT_FOUND,
USER_PROFILE_NOT_FOUND,
} from '@loopback/authentication';
const SequenceActions = RestBindings.SequenceActions;
export class MySequence implements SequenceHandler {
constructor(
@inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute,
@inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams,
@inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod,
@inject(SequenceActions.SEND) public send: Send,
@inject(SequenceActions.REJECT) public reject: Reject,
@inject(AuthenticationBindings.AUTH_ACTION)
protected authenticateRequest: AuthenticateFn,
) {}
// see: https://loopback.io/doc/en/lb4/Loopback-component-authentication.html#adding-an-authentication-action-to-a-custom-sequence
async handle(context: RequestContext) {
try {
const {request, response} = context;
const route = this.findRoute(request);
//call authentication action
console.log(`request path = ${request.path}`);
await this.authenticateRequest(request);
// Authentication step done, proceed to invoke controller
const args = await this.parseParams(request, route);
const result = await this.invoke(route, args);
this.send(response, result);
} catch (error) {
if (
error.code === AUTHENTICATION_STRATEGY_NOT_FOUND ||
error.code === USER_PROFILE_NOT_FOUND
) {
Object.assign(error, {statusCode: 401 /* Unauthorized */});
}
this.reject(context, error);
}
}
}
// Uncomment these imports to begin using these cool features!
// import {inject} from '@loopback/context';
import {get} from '@loopback/rest';
import {authenticate} from '@loopback/authentication';
import {inject} from '@loopback/core';
import {UserProfile, SecurityBindings} from '@loopback/security';
import {authorize} from '@loopback/authorization';
export class YoController {
constructor() {}
//@authenticate('my')
@authorize({allowedRoles: ['foo', 'bar'], scopes: ['asdf', 'qwer']})
@get('/yo')
yo(@inject(SecurityBindings.USER) user: UserProfile): string {
return `yo, ${user.name}!`;
}
//@authenticate('my')
@authorize({skip: true})
@get('/sup')
sup(): string {
return `sup, dude.`;
}
@get('/nope')
nope(): string {
return `sorry dude.`;
}
//@authenticate('my')
@get('/yay')
yay(
@inject(SecurityBindings.USER, {optional: true}) user: UserProfile,
): string {
if (user) {
return `yay ${user.name}!`;
}
return `yay!`;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment