Skip to content

Commit 89f0265

Browse files
committed
feat: implement connect functionality and rewrite the project
1 parent a3db437 commit 89f0265

23 files changed

+849
-108
lines changed

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,14 @@
4545
"dependencies": {
4646
"axios": "^0.19.2",
4747
"keycloak-admin": "^1.13.0",
48+
"keycloak-connect": "^10.0.2",
4849
"openid-client": "^3.15.3"
4950
},
5051
"devDependencies": {
5152
"@commitlint/cli": "^8.3.5",
5253
"@commitlint/config-conventional": "^8.3.4",
5354
"@nestjs/common": "^7.1.3",
55+
"@nestjs/core": "^7.2.0",
5456
"@semantic-release/changelog": "^5.0.1",
5557
"@semantic-release/commit-analyzer": "^8.0.1",
5658
"@semantic-release/git": "^9.0.0",

readme.md

+57-8
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ Then on your app.module.ts
2121
}),
2222
inject: [ConfigService]
2323
}),
24+
],
25+
providers: [
26+
{ provide: APP_GUARD, useClass: AuthGuard },
27+
{ provide: APP_GUARD, useClass: ResourceGuard }
2428
]
2529
})
2630
```
@@ -30,21 +34,66 @@ Then on your app.module.ts
3034
By default nestjs-keycloak-admin supports User Managed Access for managing your resources.
3135

3236
```typescript
33-
class Organization() {
37+
@Controller()
38+
@DefineResource('organization')
39+
class OrganizationController() {
3440
constructor(private readonly adminProvider: KeycloakAdminService) {}
3541

36-
async findAll(): UMAResource[] {
37-
return this.adminProvider.resourceManager.findAll()
42+
@Get('/hello')
43+
@Public()
44+
hello(): string {
45+
return 'life is short'
3846
}
3947

40-
async create(payload: payload): Promise<UMAResource> {
41-
const resource = new UMAResource(payload)
48+
@Get('/)
49+
@FetchResources()
50+
findAll(@Request('resources'): Resource[]): Resource[] {
51+
return resources
52+
}
53+
54+
@Get('/:id')
55+
@DefineScope('read') // this will check organization:read permission
56+
@DefineResourceEnforcer({
57+
id: (req: any) => req.params.id
58+
})
59+
findOne(@Request('resource'): Resource): Resource) {
60+
return resource
61+
}
62+
63+
@Get('/slug/:slug')
64+
@DefineScope('read')
65+
@DefineResourceEnforcer({
66+
id: async (req: any, context: ExecutionContext) => {
67+
const class = context.getClass<OrganizationController>()
68+
const org = await class.typeormProvider.findBySlug(req.params.slug)
69+
return org.keycloakId
70+
}
71+
})
72+
findBySlug(@Request('resource'): Resource): Resource) {
73+
return resource
74+
}
75+
76+
@Post('/')
77+
@DefineScope('create')
78+
async create((): Promise<Resource> {
79+
let resource = new Resource({
80+
name: 'resource',
81+
displayName: 'My Resource'
82+
})
4283
.setOwner(1)
43-
.addScope('organization:create')
44-
.setType('organization')
84+
.addScopes([new Scope('organization:read'), new Scope('organization:write')])
85+
.setType('urn:resource-server:type:organization')
4586
.setUri('/organization/123')
87+
.setAttributes({
88+
valid: true,
89+
types: ['customer', 'any']
90+
})
91+
92+
resource = await this.adminProvider.create(resource)
93+
94+
// create organization on your resource server and add link to resource.id, to access it later.
4695

47-
return this.adminProvider.create(resource)
96+
return resource
4897
}
4998
}
5099
```

src/interfaces.ts renamed to src/@types/package.ts

-16
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,3 @@ export interface KeycloakAdminOptions {
2323
config: KeycloakAdminConfig
2424
credentials: Credentials
2525
}
26-
27-
export interface UMAScopeOptions {
28-
name: string
29-
id?: string
30-
iconUri?: string
31-
}
32-
33-
export interface UMAResourceOptions {
34-
name: string
35-
id?: string
36-
uri?: string
37-
type?: string
38-
iconUri?: string
39-
owner?: string
40-
scopes?: string[] | UMAScopeOptions[]
41-
}

src/@types/resource.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export interface ResourceOwner {
2+
id: string
3+
}
4+
5+
export interface ResourceQuery {
6+
scope?: string
7+
type?: string
8+
uri?: string
9+
name?: string
10+
}

src/@types/uma.ticket.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export interface TicketForm {
2+
token: string
3+
audience: string
4+
grant_type?: string
5+
resourceId?: string
6+
scope?: string
7+
response_mode?: TicketResponseMode
8+
}
9+
10+
export enum TicketResponseMode {
11+
permissions = 'permissions',
12+
decision = 'decision',
13+
}
14+
15+
export interface TicketDecisionResponse {
16+
result: boolean
17+
}
18+
19+
export interface TicketPermissionResponse {
20+
scopes: string[]
21+
rsid: string
22+
rsname: string
23+
}

src/@types/uma.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { ResourceOwner } from './resource'
2+
3+
export interface UMAResource {
4+
_id?: string
5+
name: string
6+
displayName: string
7+
uris?: string[]
8+
type?: string
9+
owner?: ResourceOwner
10+
ownerManagedAccess?: boolean
11+
attributes: Record<string, any>
12+
resource_scopes?: UMAScope[]
13+
scopes: UMAScope[]
14+
}
15+
16+
export interface UMAScope {
17+
name: string
18+
id?: string
19+
icon_uri?: string
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { SetMetadata, CustomDecorator } from '@nestjs/common'
2+
3+
export const META_FETCH_RESOURCES = 'fetch-resources'
4+
5+
/**
6+
* Keycloak resource.
7+
* @param resource
8+
*/
9+
export const FetchResources = (): CustomDecorator => SetMetadata(META_FETCH_RESOURCES, true)

src/decorators/public.decorator.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { SetMetadata, CustomDecorator } from '@nestjs/common'
2+
3+
export const META_PUBLIC = 'public'
4+
5+
/**
6+
* Alias for `@Unprotected`.
7+
* @since 1.2.0
8+
*/
9+
10+
export const Public = (): CustomDecorator => SetMetadata(META_PUBLIC, true)

src/decorators/resource.decorator.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { SetMetadata, CustomDecorator } from '@nestjs/common'
2+
3+
export const META_RESOURCE = 'resource'
4+
5+
/**
6+
* Keycloak resource.
7+
* @param resource
8+
*/
9+
export const DefineResource = (resource: string): CustomDecorator =>
10+
SetMetadata(META_RESOURCE, resource)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { SetMetadata, CustomDecorator, ExecutionContext } from '@nestjs/common'
2+
3+
export const META_RESOURCE_ENFORCER = 'resource-enforcer'
4+
5+
/**
6+
* Keycloak Authorization Scopes.
7+
* @param scopes - scopes that are associated with the resource
8+
*/
9+
export const DefineResourceEnforcer = (options: ResourceDecoratorOptions): CustomDecorator =>
10+
SetMetadata(META_RESOURCE_ENFORCER, options)
11+
12+
export interface ResourceDecoratorOptions {
13+
id: ResourceDecoratorReq
14+
}
15+
16+
export interface ResourceDecoratorReq {
17+
(req: Request, context: ExecutionContext): Promise<string> | string
18+
}

src/decorators/scope.decorator.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { SetMetadata, CustomDecorator } from '@nestjs/common'
2+
3+
export const META_SCOPE = 'scope'
4+
5+
/**
6+
* Keycloak Authorization Scopes.
7+
* @param scopes - scopes that are associated with the resource
8+
*/
9+
export const DefineScope = (scope: string): CustomDecorator => SetMetadata(META_SCOPE, scope)

src/guards/auth.guard.ts

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import {
2+
CanActivate,
3+
Injectable,
4+
ExecutionContext,
5+
UnauthorizedException,
6+
Logger,
7+
} from '@nestjs/common'
8+
import { KeycloakAdminService } from '../service'
9+
import { Reflector } from '@nestjs/core'
10+
import { META_PUBLIC } from '../decorators/public.decorator'
11+
12+
@Injectable()
13+
export class AuthGuard implements CanActivate {
14+
private logger = new Logger(AuthGuard.name)
15+
16+
constructor(
17+
private readonly keycloak: KeycloakAdminService,
18+
private readonly reflector: Reflector
19+
) {}
20+
21+
async canActivate(context: ExecutionContext): Promise<boolean> {
22+
if (this.reflector.get<boolean>(META_PUBLIC, context.getHandler())) {
23+
return true
24+
}
25+
26+
const request = context.switchToHttp().getRequest()
27+
28+
const jwt = this.extractJwt(request.headers)
29+
30+
try {
31+
const result = await this.keycloak.connect.grantManager.validateAccessToken(jwt)
32+
33+
if (typeof result === 'string') {
34+
request.user = await this.keycloak.connect.grantManager.userInfo(jwt)
35+
request.accessToken = jwt
36+
return true
37+
}
38+
} catch (error) {
39+
this.logger.warn(`Error occurred validating token`, error)
40+
}
41+
42+
throw new UnauthorizedException()
43+
}
44+
45+
private extractJwt({ authorization }: Record<string, string>): string {
46+
if (!authorization) {
47+
throw new UnauthorizedException()
48+
}
49+
50+
const [type, payload] = authorization.split(' ')
51+
52+
if (type.toLowerCase() !== 'bearer') {
53+
throw new UnauthorizedException()
54+
}
55+
56+
return payload
57+
}
58+
}

0 commit comments

Comments
 (0)