

AuthN and AuthZ strategies with
NestJS and Ory
presentation by

Edouard Maleix


Backend developer,
consultant and
trainer
Node.js | NestJS | Nx

Industry Insights and Facts
Ory Overview and Storyline
Design Phase for a Greenfield Project
Ory Setup works on my machine
NestJS Integration time to code
Demo Time and end-to-end testing

AGENDA
Do you know the difference between
Authentication and
Authorization



Industry Insights
According to IBM’s Cost of a Data Breach Report 2023, half of the companies surveyed plan to expand their budget after a security incident.



According to OWASP Top 10 from 2021, broken access control is the most common security vulnerability in web applications.





Why did this happen

"For too long, authorization has been
viewed as an afterthought."
API designers and developers need
simple, efficient, cost-effective solution, based on open standards.

Poll time



Ory Overview
What is Ory?
Key components
Benefits
What is Ory?
2014
2019
2023
2024

BIRTH


Ory Network creation

Ory Network Scale up
ISO 27K and SOC2
certified


Ory Core Components








-
Self-service API
-
Custom identity schema
-
Multi-Factor Authentication
-
Admin APIs
-
Webhooks
Registration flow








Login flow













Interact with backend















- Declarative Models
- Ory Permission Language (OPL)
- Relations API
- Permissions API
Example
Douglas can edit the Document JavaScript: The Good Parts
subject permission namespace object
Document owners are a set of User
namespace relationship namespace
Document owners can edit
namespace relationship permission
User Douglas owns the Document JavaScript: The Good Parts
subject set relationship namespace object




store a document










edit a document
















Summary
Open-Source Foundation
Highly Customizable
Flexible deployment strategies
Developer-friendly
Straightforward migrations

Steep learning curve
Lack of complete tutorial
Missing a tool to make configuration files reusable
Community and Support



Design phase
What will we build?
Requirements gathering
Design considerations






User Authentication and Profile Management
User Roles and permissions
Cat Profiles Listing and Management
Fostering Matchmaking Requests
Key features
Architecture

Entities

- User: Users can be cat guardians, fosters, or both.
- CatProfile: Represents the profiles of cats available for fostering.
- Fostering: an arrangement between a user (foster) and a cat.

-
User: Users can be member of a Group (admin for instance)
- CatProfile: Users in owners and from editors Group are allowed to edit. Owners cannot foster a Cat.
- Fostering: CatProfile owners can approve and reject a request.
FosterUsers can edit Fostering activities.
Both CatProfile owners and fosterUsers can read Fostering activities.
Permissions
Ory setup


Local environment setup
Configuration reusability and replication
Tools: Docker & Docker Compose
services:
# ...
kratos:
image: oryd/kratos:v1.1.0
ports:
- '4433:4433'
- '4434:4434'
command: serve -c /etc/config/kratos/kratos.yaml --dev --watch-courier
volumes:
- ./infra/ory-kratos:/etc/config/kratos:ro
kratos-selfservice-ui-node:
image: oryd/kratos-selfservice-ui-node:v1.0.0
ports:
- '4455:4455'
keto:
image: oryd/keto:v0.12.0
ports:
- '4466:4466'
- '4467:4467'
command: serve -c /etc/config/keto/keto.yaml
volumes:
- ./infra/ory-keto:/etc/config/keto:ro
mailslurper:
image: oryd/mailslurper:latest-smtps
ports:
- '1025:1025'
- '4436:4436'
- '4437:4437'
Configure Ory Containers
some:
nested_key:
with_a_value: foo
and_array:
- id: foo
- id: bar
export SOME_NESTED_KEY_WITH_A_VALUE=foo
export SOME_NESTED_KEY_AND_ARRAY_0_ID=foo
export SOME_NESTED_KEY_AND_ARRAY_1_ID=bar
some:
nested_key:
with_a_value: "##some_nested_key_with_a_value##"
and_array: @@some_nested_key_with_a_value@@
Ory Configuration Templates
function keywordArrayReplace(input: string, mappings: KeywordMappings) {
Object.keys(mappings).forEach(function (key) {
const pattern = `@@${key}@@`;
const patternWithQuotes = `"${pattern}"`;
const regex = new RegExp(`${patternWithQuotes}|${pattern}`, 'g');
input = input.replace(regex, JSON.stringify(mappings[key]));
});
return input;
}
function keywordStringReplace(input: string, mappings: KeywordMappings) {
Object.keys(mappings).forEach(function (key) {
const regex = new RegExp(`##${key}##`, 'g');
const mapping = mappings[key];
if (
typeof mapping === 'string' ||
typeof mapping === 'number' ||
typeof mapping === 'boolean'
) {
input = input.replace(regex, mapping.toString());
} else {
input = input.replace(regex, '');
}
});
return input;
}
Ory Configuration Templates
cookies:
domain: '##kratos_cookies_domain##'
path: /
same_site: Lax
courier:
smtp:
from_name: '##kratos_courier_smtp_from_name##'
connection_uri: '##kratos_courier_smtp_connection_uri##'
dsn: '##kratos_dsn##'
feature_flags:
cacheable_sessions: false
use_continue_with_transitions: false
identity:
default_schema_id: default
schemas:
- id: default
url: '##kratos_identity_schemas_default##'
log:
level: '##kratos_log_level##'
leak_sensitive_values: true
oauth2_provider:
override_return_to: true
organizations: []
preview:
default_read_consistency_level: strong
secrets:
cookie:
- '##kratos_secrets_cookie##'
cipher:
- '##kratos_secrets_cipher##'
selfservice:
allowed_return_urls: @@kratos_selfservice_allowed_return_urls@@
default_browser_return_url: '##kratos_selfservice_default_browser_return_url##'
flows:
error:
ui_url: '##kratos_selfservice_flows_errors_ui_url##'
login:
after:
code:
hooks: []
hooks: []
lookup_secret:
hooks: []
oidc:
hooks: []
password:
hooks:
- hook: web_hook
config:
method: '##kratos_selfservice_flows_login_after_hook_config_method##'
auth:
type: api_key
config:
value: '##kratos_selfservice_flows_login_after_hook_config_auth_config_value##'
in: header
name: X-Ory-API-Key
url: '##kratos_selfservice_flows_login_after_hook_config_url##'
body: '##kratos_selfservice_flows_login_after_hook_config_body##'
can_interrupt: ##kratos_selfservice_flows_login_after_hook_config_can_interrupt##
response:
ignore: ##kratos_selfservice_flows_login_after_hook_config_response_ignore##
parse: ##kratos_selfservice_flows_login_after_hook_config_response_parse##
totp:
hooks: []
webauthn:
hooks: []
before:
hooks: []
lifespan: 30m0s
ui_url: '##kratos_selfservice_flows_login_ui_url##'
logout:
after:
default_browser_return_url: '##kratos_selfservice_default_browser_return_url##'
recovery:
after:
hooks: []
before:
hooks: []
enabled: true
lifespan: 30m0s
notify_unknown_recipients: false
ui_url: '##kratos_selfservice_flows_recovery_ui_url##'
use: code
registration:
after:
code:
hooks: []
hooks: []
oidc:
hooks:
- hook: show_verification_ui
password:
hooks:
- hook: web_hook
config:
method: '##kratos_selfservice_flows_registration_after_hook_config_method##'
auth:
type: api_key
config:
value: '##kratos_selfservice_flows_registration_after_hook_config_auth_config_value##'
in: header
name: X-Ory-API-Key
url: '##kratos_selfservice_flows_registration_after_hook_config_url##'
body: '##kratos_selfservice_flows_registration_after_hook_config_body##'
can_interrupt: ##kratos_selfservice_flows_registration_after_hook_config_can_interrupt##
response:
ignore: ##kratos_selfservice_flows_registration_after_hook_config_response_ignore##
parse: ##kratos_selfservice_flows_registration_after_hook_config_response_parse##
- hook: show_verification_ui
webauthn:
hooks:
- hook: show_verification_ui
before:
hooks: []
enabled: true
lifespan: 30m0s
login_hints: true
ui_url: '##kratos_selfservice_flows_registration_ui_url##'
settings:
after:
hooks: []
lookup_secret:
hooks: []
oidc:
hooks: []
# passkey:
# hooks: []
password:
hooks: []
profile:
hooks: []
totp:
hooks: []
webauthn:
hooks: []
before:
hooks: []
lifespan: 30m0s
privileged_session_max_age: 15m0s
required_aal: highest_available
ui_url: '##kratos_selfservice_flows_settings_ui_url##'
verification:
after:
default_browser_return_url: '##kratos_selfservice_default_browser_return_url##'
hooks: []
before:
hooks: []
enabled: true
lifespan: 30m0s
notify_unknown_recipients: false
ui_url: '##kratos_selfservice_flows_verification_ui_url##'
use: code
methods:
code:
config:
lifespan: 15m0s
enabled: true
mfa_enabled: false
passwordless_enabled: false
link:
config:
base_url: ""
lifespan: 15m0s
enabled: true
lookup_secret:
enabled: true
oidc:
config:
providers: []
enabled: false
password:
config:
haveibeenpwned_enabled: true
identifier_similarity_check_enabled: true
ignore_network_errors: true
max_breaches: 1
min_password_length: 8
enabled: true
profile:
enabled: true
totp:
config:
issuer: CatFostering
enabled: true
webauthn:
config:
passwordless: false
rp:
display_name: CatFostering
id: '##kratos_selfservice_methods_webauthn_config_rp_id##'
origins: @@kratos_selfservice_methods_webauthn_config_rp_origins@@
enabled: ##kratos_selfservice_methods_webauthn_enabled##
serve:
admin:
base_url: '##kratos_serve_admin_base_url##'
public:
base_url: '##kratos_serve_public_base_url##'
cors:
enabled: ##kratos_serve_public_cors_enabled##
allowed_origins: @@kratos_serve_public_cors_allowed_origins@@
allowed_methods:
- POST
- GET
- PUT
- PATCH
- DELETE
allowed_headers:
- Authorization
- Cookie
- Content-Type
exposed_headers:
- Content-Type
- Set-Cookie
session:
cookie:
domain: '##kratos_session_cookie_domain##'
name: '##kratos_session_cookie_name##'
path: /
persistent: true
same_site: Lax
lifespan: 72h0m0s
whoami:
required_aal: highest_available
tokenizer:
templates: {}
version: v1.1.0
Kratos configuration
{
"$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"title": "E-Mail",
"minLength": 3,
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
}
},
"verification": {
"via": "email"
},
"recovery": {
"via": "email"
}
}
}
},
"required": ["email"],
"additionalProperties": false
}
}
}
Kratos Identity Schema
function(ctx)
if std.startsWith(ctx.identity.traits.email, "test-") then
error "cancel"
else
{
identity: ctx.identity,
}
Kratos Webhooks Preprocessor
class Group implements Namespace {
related: { members: User[]; };
}
class CatProfile implements Namespace {
related: {
owners: User[];
editors: SubjectSet<Group, 'members'>[];
};
permits = {
edit: (ctx: Context) =>
this.related.owners.includes(ctx.subject) ||
this.related.editors.includes(ctx.subject),
foster: (ctx: Context) => !this.related.owners.includes(ctx.subject),
};
}
class Fostering implements Namespace {
related: {
catProfiles: CatProfile[];
fosterUsers: User[];
};
permits = {
approve: (ctx: Context) =>
this.related.catProfiles.traverse((cp) =>
cp.related.owners.includes(ctx.subject)
),
reject: (ctx: Context) => this.permits.approve(ctx),
edit: (ctx: Context) => this.related.fosterUsers.includes(ctx.subject),
read: (ctx: Context) => this.permits.approve(ctx) || this.permits.edit(ctx),
};
}
Keto Permissions

Configuring NestJS application
Ory Webhooks handling
User registration
Relationships declaration
Routes protection

NestJS Integration


export class EnvironmentVariables {
// ...
@Expose()
@IsUrl(urlOptions)
ORY_KETO_ADMIN_URL?: string = 'http://localhost:4467';
@Expose()
@IsUrl(urlOptions)
ORY_KETO_PUBLIC_URL?: string = 'http://localhost:4466';
@SecretValue({ isOptional: true })
ORY_KETO_API_KEY?: string = null;
@Expose()
@IsUrl(urlOptions)
@IsOptional()
ORY_KRATOS_ADMIN_URL?: string = 'http://localhost:4434';
@Expose()
@IsUrl(urlOptions)
@IsOptional()
ORY_KRATOS_PUBLIC_URL?: string = 'http://localhost:4433';
@SecretValue({ isOptional: true })
ORY_KRATOS_API_KEY?: string = null;
@SecretValue()
ORY_ACTION_API_KEY: string;
}
Configure NestJS application
Ory Kratos is configured to send 2 types of events to our application:
After registration
After login
Handle Webhooks


@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@UseGuards(OryActionGuard)
@UsePipes(
new ValidationPipe({
transform: true,
forbidUnknownValues: true,
exceptionFactory: (errors) => {
const formattedErrors = validationErrorsToOryErrorMessages(errors);
return new OryWebhookError(
'Failed to validate input',
formattedErrors,
400
);
},
})
)
@Post('on-sign-up')
@HttpCode(HttpStatus.OK)
onSignUp(@Body() body: OnOrySignUpDto): Promise<OnOrySignUpDto> {
return this.usersService.onSignUp(body);
}
//...
}

After registration
@Injectable()
export class OryActionGuard implements CanActivate {
readonly logger = new Logger(OryActionGuard.name);
private readonly apiKey: string;
constructor(
@Inject(ConfigService)
configService: ConfigService<{ ORY_ACTION_API_KEY: string }, true>
) {
this.apiKey = configService.get('ORY_ACTION_API_KEY');
}
canActivate(context: ExecutionContext): boolean {
const req = context.switchToHttp().getRequest();
const authHeader = req.headers['x-ory-api-key'];
return (
!!authHeader &&
timingSafeEqual(Buffer.from(authHeader), Buffer.from(this.apiKey))
);
}
}
Ory Action Guard
import { HttpException } from '@nestjs/common';
export type OryWebhookErrorMessages = {
instance_ptr?: string;
messages: {
id: number;
text: string;
type: string;
context?: {
value: string;
};
}[];
};
export class OryWebhookError extends HttpException {
constructor(
message: string,
public readonly messages: OryWebhookErrorMessages[],
status: number
) {
super(OryWebhookError.toJSON(messages, status), status);
this.name = 'OryWebhookError';
this.message = message;
Error.captureStackTrace(this, OryWebhookError);
}
static toJSON(messages: OryWebhookErrorMessages[], statusCode: number) {
return {
statusCode,
messages,
};
}
}
Ory Webhook Error


Custom form errors
export class UsersService {
async onSignUp(body: OnOrySignUpDto): Promise<OnOrySignUpDto> {
this.logger.debug(inspect({ method: 'onSignUp', body }, false, null, true));
const { email } = body.identity.traits;
const existingUser = await this.userRepository.findOne({
where: { email },
});
if (existingUser) {
throw new OryWebhookError(
'email already used',
[
{
instance_ptr: '#/traits/email',
messages: [
{
id: 123,
text: 'invalid email address',
type: 'validation',
context: {
value: email,
},
},
],
},
],
HttpStatus.BAD_REQUEST
);
}
const result = await this.userRepository.save({ email });
body.identity.metadata_public = { id: result.id };
return { identity: body.identity };
}
// ...
}
Ory identity mutation

Verify user identity
@Controller('users')
export class UsersController {
@UseGuards(
OryAuthenticationGuard({
cookieResolver: (ctx) =>
ctx.switchToHttp().getRequest<Request>().headers.cookie ?? '',
sessionTokenResolver: (ctx) =>
ctx
.switchToHttp()
.getRequest<Request>()
.headers?.authorization?.replace('Bearer ', '') ?? '',
postValidationHook: (ctx, session) => {
if (!isValidOrySession(session)) {
throw new HttpException('Invalid session', HttpStatus.UNAUTHORIZED);
}
const request = ctx.switchToHttp().getRequest();
request.user = {
id: session.identity.metadata_public['id'],
email: session.identity.traits.email,
identityId: session.identity.id,
};
},
})
)
@Get('current-user')
getCurrentUser(@CurrentUser() user: User): User {
return user;
}
}
Check user's session

Create relationships
@Controller('cat-profiles')
export class CatProfilesController {
constructor(private readonly catProfilesService: CatProfilesService) {}
// ...
@UseGuards(AuthenticationGuard())
@UsePipes(
new ValidationPipe({
transform: true,
transformOptions: { enableImplicitConversion: true },
})
)
@Post()
create(
@CurrentUser() user: CurrentUser,
@Body() body: CreateCatProfile
): Promise<CatProfile> {
return this.catProfilesService.create(body, user.id);
}
//...
}
Create a Cat Profile - Controller
export class CatProfilesService {
// ...
async create(body: CreateCatProfile, userId: string) {
const queryRunner =
this.catProfileRepository.manager.connection.createQueryRunner();
let catProfile = queryRunner.manager.create(CatProfileSchema, {
...body,
owner: { id: userId },
});
await queryRunner.connect();
await queryRunner.startTransaction();
try {
catProfile = await queryRunner.manager.save(catProfile);
await this.createAdminRelationship(catProfile.id);
await this.createOwnerRelationship(catProfile.id, userId);
await queryRunner.commitTransaction();
return catProfile;
} catch (err) {
this.logger.error(err);
await queryRunner.rollbackTransaction();
if (catProfile.id) {
await this.deleteOwnerRelationship(catProfile.id, userId);
await this.deleteAdminRelationship(catProfile.id);
}
throw new HttpException(
'Failed to create cat profile',
HttpStatus.BAD_REQUEST
);
} finally {
await queryRunner.release();
}
}
//...
}
Create a Cat Profile - Service

Check permissions
@Controller('cat-profiles')
export class CatProfilesController {
constructor(private readonly catProfilesService: CatProfilesService) {}
@OryPermissionChecks({
type: 'OR',
conditions: [isOwnerPermission, isAdminPermission],
})
@UseGuards(
AuthenticationGuard(),
OryAuthorizationGuard({
unauthorizedFactory(ctx) {
return new HttpException(
'Forbidden',
403,
ctx.switchToHttp().getRequest().url
);
},
})
)
@UsePipes(
new ValidationPipe({
transform: true,
transformOptions: { enableImplicitConversion: true },
})
)
@Patch(':id')
updateById(
@Param('id', ParseUUIDPipe) id: string,
@Body() body: UpdateCatProfile
): Promise<CatProfile> {
return this.catProfilesService.updateById(id, body);
}
Update a Cat Profile - Controller
import { CurrentUser, getCurrentUser } from '@cat-fostering/nestjs-utils';
import { relationTupleBuilder } from '@getlarge/keto-relations-parser';
import type { ExecutionContext } from '@nestjs/common';
import type { Request } from 'express';
// returns Group:admin#members@User:<currentUserId>
export const isAdminPermission = (ctx: ExecutionContext): string => {
const req = ctx.switchToHttp().getRequest<Request & { user: CurrentUser }>();
const currentUserId = getCurrentUser(req).id;
return relationTupleBuilder()
.subject('User', currentUserId)
.isIn('members')
.of('Group', 'admin')
.toString();
};
// returns CatProfile:<catProfileId>#owners@User:<currentUserId>
export const isOwnerPermission = (ctx: ExecutionContext): string => {
const req = ctx.switchToHttp().getRequest<Request & { user: CurrentUser }>();
const currentUserId = getCurrentUser(req).id;
const catProfileId = req.params['id'];
return relationTupleBuilder()
.subject('User', currentUserId)
.isIn('owners')
.of('CatProfile', catProfileId)
.toString();
};
Evalute permission from request context
Time for some action


Setting up the project
Create users and admin
Interact with CatProfiles
Test automation
nano .env
nano apps/cat-fostering-api/.env
npm run ory:generate
npm run docker:dev:up
npx nx run cat-fostering-api:serve
Setup steps
BACKEND_DOCKER_HOST="http://host.docker.internal:3000"
BACKEND_HOST="http://127.0.0.1:3000"
KRATOS_PUBLIC_HOST="http://127.0.0.1:4433"
SELF_SERVICE_UI_HOST="http://127.0.0.1:4455"
ORY_ACTION_API_KEY="fetfek-fizNow-guqby0"
kratos_cookies_domain="127.0.0.1"
kratos_courier_smtp_connection_uri="smtps://test:test@mailslurper:1025/?skip_ssl_verify=true"
kratos_dsn="postgres://dbuser:secret@kratos-postgres:5432/kratosdb?sslmode=disable"
kratos_identity_schemas_default="base64://ewogICIkaWQiOiAiaHR0cHM6Ly9zY2hlbWFzLm9yeS5zaC9wcmVzZXRzL2tyYXRvcy9xdWlja3N0YXJ0L2VtYWlsLXBhc3N3b3JkL2lkZW50aXR5LnNjaGVtYS5qc29uIiwKICAiJHNjaGVtYSI6ICJodHRwOi8vanNvbi1zY2hlbWEub3JnL2RyYWZ0LTA3L3NjaGVtYSMiLAogICJ0aXRsZSI6ICJQZXJzb24iLAogICJ0eXBlIjogIm9iamVjdCIsCiAgInByb3BlcnRpZXMiOiB7CiAgICAidHJhaXRzIjogewogICAgICAidHlwZSI6ICJvYmplY3QiLAogICAgICAicHJvcGVydGllcyI6IHsKICAgICAgICAiZW1haWwiOiB7CiAgICAgICAgICAidHlwZSI6ICJzdHJpbmciLAogICAgICAgICAgImZvcm1hdCI6ICJlbWFpbCIsCiAgICAgICAgICAidGl0bGUiOiAiRS1NYWlsIiwKICAgICAgICAgICJtaW5MZW5ndGgiOiAzLAogICAgICAgICAgIm9yeS5zaC9rcmF0b3MiOiB7CiAgICAgICAgICAgICJjcmVkZW50aWFscyI6IHsKICAgICAgICAgICAgICAicGFzc3dvcmQiOiB7CiAgICAgICAgICAgICAgICAiaWRlbnRpZmllciI6IHRydWUKICAgICAgICAgICAgICB9CiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJ2ZXJpZmljYXRpb24iOiB7CiAgICAgICAgICAgICAgInZpYSI6ICJlbWFpbCIKICAgICAgICAgICAgfSwKICAgICAgICAgICAgInJlY292ZXJ5IjogewogICAgICAgICAgICAgICJ2aWEiOiAiZW1haWwiCiAgICAgICAgICAgIH0KICAgICAgICAgIH0KICAgICAgICB9CiAgICAgIH0sCiAgICAgICJyZXF1aXJlZCI6IFsiZW1haWwiXSwKICAgICAgImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjogZmFsc2UKICAgIH0KICB9Cn0K"
kratos_log_level="trace"
kratos_selfservice_allowed_return_urls="${BACKEND_HOST}, ${SELF_SERVICE_UI_HOST}"
kratos_selfservice_default_browser_return_url="${SELF_SERVICE_UI_HOST}/"
kratos_selfservice_flows_errors_ui_url="${SELF_SERVICE_UI_HOST}/error"
kratos_selfservice_flows_login_after_hook_config_url="${BACKEND_DOCKER_HOST}/api/users/on-sign-in"
kratos_selfservice_flows_login_after_hook_config_auth_config_value="${ORY_ACTION_API_KEY}"
kratos_selfservice_flows_login_after_hook_config_body="base64://ZnVuY3Rpb24oY3R4KQppZiBzdGQuc3RhcnRzV2l0aChjdHguaWRlbnRpdHkudHJhaXRzLmVtYWlsLCAidGVzdC0iKSB0aGVuCiAgZXJyb3IgImNhbmNlbCIKZWxzZQogIHsKICAgIGlkZW50aXR5OiBjdHguaWRlbnRpdHksCiAgfQo="
kratos_selfservice_flows_login_after_hook_config_can_interrupt="true"
kratos_selfservice_flows_login_after_hook_config_response_ignore="false"
kratos_selfservice_flows_login_after_hook_config_response_parse="false"
kratos_selfservice_flows_login_ui_url="${SELF_SERVICE_UI_HOST}/login"
kratos_selfservice_flows_recovery_ui_url="${SELF_SERVICE_UI_HOST}/recovery"
kratos_selfservice_flows_registration_after_hook_config_url="${BACKEND_DOCKER_HOST}/api/users/on-sign-up"
kratos_selfservice_flows_registration_after_hook_config_auth_config_value="${ORY_ACTION_API_KEY}"
kratos_selfservice_flows_registration_after_hook_config_body="base64://ZnVuY3Rpb24oY3R4KQppZiBzdGQuc3RhcnRzV2l0aChjdHguaWRlbnRpdHkudHJhaXRzLmVtYWlsLCAidGVzdC0iKSB0aGVuCiAgZXJyb3IgImNhbmNlbCIKZWxzZQogIHsKICAgIGlkZW50aXR5OiBjdHguaWRlbnRpdHksCiAgfQo="
kratos_selfservice_flows_registration_after_hook_config_can_interrupt="true"
kratos_selfservice_flows_registration_after_hook_config_response_ignore="false"
kratos_selfservice_flows_registration_after_hook_config_response_parse="true"
kratos_selfservice_flows_registration_ui_url="${SELF_SERVICE_UI_HOST}/registration"
kratos_selfservice_flows_settings_ui_url="${SELF_SERVICE_UI_HOST}/settings"
kratos_selfservice_flows_verification_ui_url="${SELF_SERVICE_UI_HOST}/verification"
kratos_selfservice_methods_passkey_config_rp_id="localhost"
kratos_selfservice_methods_passkey_config_rp_origins="https://localhost:8080"
kratos_selfservice_methods_passkey_enabled="false"
kratos_selfservice_methods_webauthn_config_rp_id="localhost"
kratos_selfservice_methods_webauthn_config_rp_origins="https://localhost:8080"
kratos_selfservice_methods_webauthn_enabled="false"
kratos_secrets_cookie="cookie_secret_not_good_not_secure"
kratos_secrets_cipher="32-LONG-SECRET-NOT-SECURE-AT-ALL"
kratos_serve_admin_base_url="http://kratos:4434/"
kratos_serve_public_base_url="${KRATOS_PUBLIC_HOST}/"
kratos_serve_public_cors_enabled="true"
kratos_serve_public_cors_allowed_origins="${KRATOS_PUBLIC_HOST}, ${SELF_SERVICE_UI_HOST}, ${BACKEND_HOST}"
kratos_session_cookie_domain="127.0.0.1"
keto_dsn="postgres://dbuser:secret@keto-postgres:5432/accesscontroldb?sslmode=disable"
keto_log_level="trace"
keto_namespaces_location="base64://aW1wb3J0IHR5cGUgewogIE5hbWVzcGFjZSwKICBDb250ZXh0LAogIFN1YmplY3RTZXQsCiAgLy8gQHRzLWV4cGVjdC1lcnJvciAtIFRoaXMgaXMgYSBwcml2YXRlIHR5cGUgZnJvbSB0aGUgaW50ZXJuYWwgT3J5IEtldG8gU0RLCn0gZnJvbSAnQG9yeS9wZXJtaXNzaW9uLW5hbWVzcGFjZS10eXBlcyc7CgpjbGFzcyBVc2VyIGltcGxlbWVudHMgTmFtZXNwYWNlIHt9CgpjbGFzcyBHcm91cCBpbXBsZW1lbnRzIE5hbWVzcGFjZSB7CiAgcmVsYXRlZDogewogICAgbWVtYmVyczogVXNlcltdOwogIH07Cn0KCmNsYXNzIENhdFByb2ZpbGUgaW1wbGVtZW50cyBOYW1lc3BhY2UgewogIHJlbGF0ZWQ6IHsKICAgIG93bmVyczogVXNlcltdOwogICAgZWRpdG9yczogU3ViamVjdFNldDxHcm91cCwgJ21lbWJlcnMnPltdOwogIH07CgogIHBlcm1pdHMgPSB7CiAgICBlZGl0OiAoY3R4OiBDb250ZXh0KSA9PgogICAgICB0aGlzLnJlbGF0ZWQub3duZXJzLmluY2x1ZGVzKGN0eC5zdWJqZWN0KSB8fAogICAgICB0aGlzLnJlbGF0ZWQuZWRpdG9ycy5pbmNsdWRlcyhjdHguc3ViamVjdCksCiAgICBmb3N0ZXI6IChjdHg6IENvbnRleHQpID0+ICF0aGlzLnJlbGF0ZWQub3duZXJzLmluY2x1ZGVzKGN0eC5zdWJqZWN0KSwKICB9Owp9CgpjbGFzcyBGb3N0ZXJpbmcgaW1wbGVtZW50cyBOYW1lc3BhY2UgewogIHJlbGF0ZWQ6IHsKICAgIGNhdFByb2ZpbGVzOiBDYXRQcm9maWxlW107CiAgICBmb3N0ZXJVc2VyczogVXNlcltdOwogIH07CgogIHBlcm1pdHMgPSB7CiAgICBhcHByb3ZlOiAoY3R4OiBDb250ZXh0KSA9PgogICAgICAvLyBAdHMtZXhwZWN0LWVycm9yIC0gVGhpcyBpcyBhIHByaXZhdGUgdHlwZSBmcm9tIHRoZSBpbnRlcm5hbCBPcnkgS2V0byBTREsKICAgICAgdGhpcy5yZWxhdGVkLmNhdFByb2ZpbGVzLnRyYXZlcnNlKChjcCkgPT4KICAgICAgICBjcC5yZWxhdGVkLm93bmVycy5pbmNsdWRlcyhjdHguc3ViamVjdCkKICAgICAgKSwKICAgIHJlamVjdDogKGN0eDogQ29udGV4dCkgPT4gdGhpcy5wZXJtaXRzLmFwcHJvdmUoY3R4KSwKICAgIGVkaXQ6IChjdHg6IENvbnRleHQpID0+IHRoaXMucmVsYXRlZC5mb3N0ZXJVc2Vycy5pbmNsdWRlcyhjdHguc3ViamVjdCksCiAgICByZWFkOiAoY3R4OiBDb250ZXh0KSA9PiB0aGlzLnBlcm1pdHMuYXBwcm92ZShjdHgpIHx8IHRoaXMucGVybWl0cy5lZGl0KGN0eCksCiAgfTsKfQo="
Environment variables
Self-Service UI

npx @getlarge/kratos-cli register --email admin@getlarge.eu
npx @getlarge/kratos-cli register --email nobody@getlarge.eu
# verify email addresses
npx @getlarge/kratos-cli login --email admin@getlarge.eu
export ORY_SESSION_TOKEN=<token>
curl -X GET \
'http://localhost:3000/api/users/current-user' \
--header "Authorization: Bearer $ORY_SESSION_TOKEN"
npx @getlarge/keto-cli create --tuple Group:admin#members@User:<id>
Create an Admin user
Create an Admin user
Testing the CatProfiles API
Testing the Fostering API
Let's create an end-to-end tests suite.
- Quick adaptation of Ory configurations for testing.
- Use transient data only
- Simplify user creation by turning off email verification.
- Use Jest hooks to init and clean Ory services and DB
Automated Testing
import { execSync } from 'node:child_process';
import { createTestConnection } from './helpers';
const envPath = 'apps/cat-fostering-api/.env.test';
const cwd = process.cwd();
export default async (): Promise<void> => {
console.log('\nSetting up...\n');
execSync(
'npx ts-node --project tools/tsconfig.json tools/ory/generate-config.ts keto -e .env.ci',
{ cwd, stdio: 'ignore' }
);
execSync('docker compose restart keto', { cwd, stdio: 'ignore' });
execSync(
'npx ts-node --project tools/tsconfig.json tools/ory/generate-config.ts kratos -e .env.ci',
{ cwd, stdio: 'ignore' }
);
execSync('docker compose restart kratos', { cwd, stdio: 'ignore' });
globalThis.__DB_CONNECTION__ = await createTestConnection(envPath);
};
Jest Setup
import { execSync } from 'node:child_process';
import { DataSource } from 'typeorm';
const cwd = process.cwd();
export default async (): Promise<void> => {
console.log('Cleaning up...');
await (globalThis.__DB_CONNECTION__ as DataSource)?.destroy();
execSync(
'npx ts-node --project tools/tsconfig.json tools/ory/generate-config.ts keto -e .env',
{ cwd, stdio: 'ignore' }
);
execSync('docker compose restart keto', { cwd, stdio: 'ignore' });
execSync(
'npx ts-node --project tools/tsconfig.json tools/ory/generate-config.ts kratos -e .env',
{ cwd, stdio: 'ignore' }
);
execSync('docker compose restart kratos', { cwd, stdio: 'ignore' });
};
Jest Teardown
describe('E2E API tests', () => {
let user1: TestUser;
let user2: TestUser;
beforeAll(async () => {
user1 = await createOryUser({
email: 'admin@test.it',
password: 'p4s$worD!',
});
createOryAdminRelation({ userId: user1.id });
user2 = await createOryUser({
email: 'user@test.it',
password: 'p4s$worD!',
});
});
describe('GET /api/users/current-user', () => {
it('should return the current user', async () => {
const res = await axios.get(`/api/users/current-user`, {
headers: {
Authorization: `Bearer ${user1.sessionToken}`,
},
});
expect(res.status).toBe(200);
expect(res.data.email).toBe(user1.email);
});
it('should return 401 if no token is provided', async () => {
const res = await axios.get(`/api/users/current-user`);
expect(res.status).toBe(401);
});
});
describe('POST /api/cat-profiles', () => {
it('should create a cat profile', async () => {
const res = await axios.post(
`/api/cat-profiles`,
{
name: 'Godard',
description: 'Black and white cat, knows how to open doors',
age: 3,
},
axiosOptionsFactory(user1.sessionToken)
);
expect(res.status).toBe(201);
});
});
describe('PATCH /api/cat-profiles/:id', () => {
it('should update a cat profile when user is the owner', async () => {
const cat = await createCat({
name: 'Romeo',
description: 'Grey cat, loves to cuddle',
age: 2,
sessionToken: user1.sessionToken,
});
const res = await axios.patch(
`/api/cat-profiles/${cat.id}`,
{ age: 3 },
axiosOptionsFactory(user1.sessionToken)
);
expect(res.status).toBe(200);
});
it('should update a cat profile when user is an admin', async () => {
const cat = await createCat({
name: 'Juliet',
description: 'White cat, loves to play',
age: 1,
sessionToken: user2.sessionToken,
});
const res = await axios.patch(
`/api/cat-profiles/${cat.id}`,
{ age: 2 },
axiosOptionsFactory(user1.sessionToken)
);
expect(res.status).toBe(200);
});
it(`should return 403 if the user is not an admin or the cat profile's owner`, async () => {
const cat = await createCat({
name: 'Crousti',
description: 'Tabby brown, with a diva attitude',
age: 8,
sessionToken: user1.sessionToken,
});
const res = await axios.patch(
`/api/cat-profiles/${cat.id}`,
{ age: 9 },
axiosOptionsFactory(user2.sessionToken)
);
expect(res.status).toBe(403);
});
});
})
Write E2E tests
Run the tests

Conclusion
Migration challenges
Manage environments
External resources
- Users migration
-
Permissions migrations
Migration Challenges
Users migration
- Assess the user pool data (access to password hashes?)
- Map the existing user attributes to the corresponding fields in the Ory Kratos identity schema
- Import the user accounts using the Identity API
- Monitor the user migration for any issues and resolve them as needed.
Permissions migrations
- Identify a permission
- Setup automated end-to-end tests
- Map the existing permission to the new policy set in Oro Keto
- Test the permissions mapping
- Deploy the migration to the production
- Repeat the process for all the existing policies
Manage multiple environments
- Stick with Ory self-hosting for local development and testing
- Move to Ory Network for staging and production
- Synchronize and version environment variables in a vault such as Dotenv.org
Resources








Deploy to Ory Network
Moving from self-hosting to Ory Network.
- Benefits of Ory Network
- Transition steps

Ory Network
- Global Multi-Region Architecture
- Commitment to Compliance
- Reliability
- Tenant Management
- Configure self-service UI
Bridging the Gap Between Open-Source and Cloud
Transition steps
- We start by registering an account with Ory Network.
- Create a new workspace to organize your projects.
- Register a new project for each stage
- Install the Ory CLI to configure your project
- Create script to generate project configuration from existing template files

Build tools to configure the project
- File references pointing to local files (file:// protocol) won't work
- URLs must resolve to public addresses.
- Keys for server configuration are ignored
type OryNetworkConfig = {
name: string;
services: {
identity: {
config: Record<string, unknown>;
};
permission: {
config: Record<string, unknown>;
};
};
};
Transition steps

AuthN and AuthZ in NestJS with Ory
By edouard_maleix
AuthN and AuthZ in NestJS with Ory
According to OWASP 10, broken access control is web applications' most common security vulnerability. In this context, it is more crucial than ever for (NestJS) developers to be empowered with solutions that enhance their application's security without sacrificing efficiency. This presentation focuses on integrating Ory, an open-source identity and access control management toolkit, with NestJS to provide a complete security solution.
- 379