Skip to content

Commit f2fd812

Browse files
committed
support client certificate
1 parent 23743f4 commit f2fd812

File tree

5 files changed

+253
-11
lines changed

5 files changed

+253
-11
lines changed

src/services/keycloak/client-registration-service.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { headers } from './keycloak-api';
77
import { strict as assert } from 'assert';
88

99
import { clientTemplateClientSecret } from './templates/client-template-client-secret';
10+
import { clientTemplateClientCertificate } from './templates/client-template-client-certificate';
1011
import { clientTemplateClientJwt } from './templates/client-template-client-jwt';
1112
import { clientTemplateSharedIdP } from './templates/client-template-shared-idp';
1213
import { clientTemplateSharedIdPAuthz } from './templates/client-template-shared-idp-authz';
@@ -38,6 +39,7 @@ export enum ClientAuthenticator {
3839
ClientJWT = 'client-jwt',
3940
ClientJWTwithJWKS = 'client-jwt-jwks-url',
4041
ClientSecret = 'client-secret',
42+
ClientCertificate = 'client-certificate',
4143
SharedIdP = 'shared-idp',
4244
SharedIdPWithAuthz = 'shared-idp-authz',
4345
}
@@ -94,6 +96,13 @@ export class KeycloakClientRegistrationService {
9496
},
9597
});
9698
break;
99+
case ClientAuthenticator.ClientCertificate:
100+
body = Object.assign(JSON.parse(clientTemplateClientCertificate), {
101+
enabled,
102+
clientId,
103+
secret: clientSecret,
104+
});
105+
break;
97106
case ClientAuthenticator.SharedIdP:
98107
body = Object.assign(JSON.parse(clientTemplateSharedIdP), {
99108
enabled,
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
2+
export const clientTemplateClientCertificate = JSON.stringify({
3+
clientId: '',
4+
name: '',
5+
description: '',
6+
surrogateAuthRequired: false,
7+
enabled: false,
8+
alwaysDisplayInConsole: false,
9+
clientAuthenticatorType: 'client-x509',
10+
redirectUris: ['http://*', 'https://*'],
11+
webOrigins: ['*'],
12+
notBefore: 0,
13+
bearerOnly: false,
14+
consentRequired: false,
15+
standardFlowEnabled: true,
16+
implicitFlowEnabled: false,
17+
directAccessGrantsEnabled: false,
18+
serviceAccountsEnabled: true,
19+
publicClient: false,
20+
frontchannelLogout: false,
21+
protocol: 'openid-connect',
22+
attributes: {
23+
"request.object.signature.alg": "any",
24+
"saml.multivalued.roles": "false",
25+
"saml.force.post.binding": "false",
26+
"oauth2.device.authorization.grant.enabled": "false",
27+
"backchannel.logout.revoke.offline.tokens": "false",
28+
"saml.server.signature.keyinfo.ext": "false",
29+
"use.refresh.tokens": "true",
30+
"realm_client": "false",
31+
"oidc.ciba.grant.enabled": "false",
32+
"backchannel.logout.session.required": "true",
33+
"client_credentials.use_refresh_token": "false",
34+
"saml.client.signature": "false",
35+
"require.pushed.authorization.requests": "false",
36+
"request.object.encryption.enc": "any",
37+
"dpop.bound.access.tokens": "false",
38+
"saml.assertion.signature": "false",
39+
"x509.subjectdn": "CN=sdx-pub-icbc-sap.api.gov.bc.ca",
40+
"request.object.encryption.alg": "any",
41+
"client.introspection.response.allow.jwt.claim.enabled": "false",
42+
"saml.encrypt": "false",
43+
"standard.token.exchange.enabled": "true",
44+
"saml.server.signature": "false",
45+
"exclude.session.state.from.auth.response": "false",
46+
"client.use.lightweight.access.token.enabled": "false",
47+
"request.object.required": "not required",
48+
"saml_force_name_id_format": "false",
49+
"access.token.header.type.rfc9068": "false",
50+
"acr.loa.map": "{}",
51+
"tls.client.certificate.bound.access.tokens": "true",
52+
"saml.authnstatement": "false",
53+
"display.on.consent.screen": "false",
54+
"x509.allow.regex.pattern.comparison": "false",
55+
"token.response.type.bearer.lower-case": "false",
56+
"saml.onetimeuse.condition": "false"
57+
},
58+
authenticationFlowBindingOverrides: {},
59+
fullScopeAllowed: false,
60+
nodeReRegistrationTimeout: -1,
61+
protocolMappers: [] as any[],
62+
defaultClientScopes: [] as string[],
63+
optionalClientScopes: [] as string[],
64+
access: { view: true, configure: true, manage: true },
65+
});

src/services/keystone/access-request.ts

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,103 @@
11
import { gql } from 'graphql-request';
22
import { Logger } from '../../logger';
3-
import { AccessRequest, AccessRequestUpdateInput } from './types';
3+
import {
4+
AccessRequest,
5+
AccessRequestCreateInput,
6+
AccessRequestUpdateInput,
7+
} from './types';
48

59
const assert = require('assert').strict;
610
const logger = Logger('keystone.access-req');
711

12+
/*
13+
acceptLegal
14+
:
15+
false
16+
additionalDetails
17+
:
18+
""
19+
applicationId
20+
:
21+
"2"
22+
controls
23+
:
24+
"{\"clientGenCertificate\":false,\"jwksUrl\":\"\",\"clientCertificate\":\"\"}"
25+
name
26+
:
27+
"Sample API FOR Cope, Aidan CITZ:EX"
28+
productEnvironmentId
29+
:
30+
"12"
31+
requestor
32+
:
33+
"12"*/
34+
35+
export async function addAccessRequest(
36+
context: any,
37+
data: any
38+
): Promise<AccessRequest> {
39+
const query = gql`
40+
mutation AddAccessRequest(
41+
$name: String!
42+
$controls: String
43+
$requestor: ID!
44+
$applicationId: ID!
45+
$productEnvironmentId: ID!
46+
$additionalDetails: String
47+
$acceptLegal: Boolean!
48+
) {
49+
acceptLegal(
50+
productEnvironmentId: $productEnvironmentId
51+
acceptLegal: $acceptLegal
52+
) {
53+
legalsAgreed
54+
}
55+
56+
createAccessRequest(
57+
data: {
58+
name: $name
59+
controls: $controls
60+
additionalDetails: $additionalDetails
61+
requestor: { connect: { id: $requestor } }
62+
application: { connect: { id: $applicationId } }
63+
productEnvironment: { connect: { id: $productEnvironmentId } }
64+
}
65+
) {
66+
id
67+
}
68+
}
69+
`;
70+
71+
logger.debug('Mutation [addAccessRequest] data %j', data);
72+
const result = await context.executeGraphQL({
73+
query,
74+
variables: { ...data },
75+
});
76+
logger.debug('Mutation [addAccessRequest] result %j', result);
77+
return result.data.createAccessRequest;
78+
}
79+
80+
export async function collectCredentials(context: any, id: string): Promise<AccessRequest> {
81+
logger.debug('Collecting credentials for access request %s', id);
82+
const query = gql`
83+
mutation genCredential($id: ID!) {
84+
updateAccessRequest(id: $id, data: { credential: "NEW" }) {
85+
credential
86+
}
87+
}`
88+
const result = await context.executeGraphQL({
89+
query,
90+
variables: { id },
91+
});
92+
logger.debug('Mutation [collectCredentials] result %j', result);
93+
assert.strictEqual(
94+
'errors' in result,
95+
false,
96+
'Error collecting credentials'
97+
);
98+
return result.data.updateAccessRequest;
99+
}
100+
8101
export async function getAccessRequestsByNamespace(
9102
context: any,
10103
ns: string

src/services/keystone/application.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,26 @@ export async function lookupMyApplicationsById(
4141
logger.debug('[lookupMyApplicationsById] result %j', result);
4242
return result.data.myApplications[0];
4343
}
44+
45+
46+
export async function createApplication(
47+
context: any,
48+
data: { name: string, ownerId: string, description?: string }
49+
): Promise<Application> {
50+
logger.debug('[createApplication] %j', data);
51+
const result = await context.executeGraphQL({
52+
query: `mutation CreateApplication($name: String!, $description: String, $ownerId: ID!) {
53+
createApplication(data: {name: $name, owner: {connect: {id: $ownerId}}, description: $description}) {
54+
id
55+
appId
56+
name
57+
owner {
58+
name
59+
}
60+
}
61+
}`,
62+
variables: data,
63+
});
64+
logger.debug('[createApplication] result %j', result);
65+
return result.data.createApplication;
66+
}

src/test/integrated/keystonejs/accessRequest.ts

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,53 @@ To run:
44
npm run ts-build
55
npm run ts-watch
66
node dist/test/integrated/keystonejs/accessRequest.js
7+
8+
9+
NEEDS:
10+
11+
kubectl port-forward -n 1d4461-prod service/patroni-spilo 15432:5432 &
12+
13+
export ADAPTER=knex
14+
export KNEX_DATABASE=keystonejs
15+
export KNEX_HOST=localhost
16+
export KNEX_PORT=15432
17+
export KNEX_USER=keystonejsuser
18+
export KNEX_PASSWORD=
19+
20+
21+
export KONG_URL="https://kong-admin-api-1d4461-prod.apps.silver.devops.gov.bc.ca"
22+
23+
kubectl port-forward -n 1d4461-prod service/bcgov-aps-portal-feeder-generic-api 6767:80 &
24+
25+
export FEEDER_URL=http://localhost:6767
26+
27+
// userId is needed for Legal
28+
// namespace has to match requesting product if not published
29+
730
*/
831

932
import InitKeystone from './init';
1033
import { o } from '../util';
11-
import { getOpenAccessRequestsByConsumer } from '../../../services/keystone/access-request';
34+
import { addAccessRequest, collectCredentials, getOpenAccessRequestsByConsumer } from '../../../services/keystone/access-request';
35+
import { add } from 'lodash';
36+
import { AccessRequestCreateInput } from 'apis/shared/types/query.types';
37+
import { createApplication } from '../../../services/keystone/application';
1238

1339
(async () => {
1440
const keystone = await InitKeystone();
1541

16-
const ns = 'gw-0dcd7';
17-
const skipAccessControl = false;
42+
const ns = 'gw-0a524';
43+
const skipAccessControl = true;
44+
45+
const userId = '12';
1846

1947
const identity = {
2048
id: null,
2149
username: 'sample_username',
2250
namespace: ns,
2351
roles: JSON.stringify(['api-owner']),
2452
scopes: [],
25-
userId: null,
53+
userId,
2654
} as any;
2755

2856
const ctx = keystone.createContext({
@@ -32,12 +60,36 @@ import { getOpenAccessRequestsByConsumer } from '../../../services/keystone/acce
3260

3361
// o(await getOrganizations(ctx));
3462

35-
const serviceAccess = await getOpenAccessRequestsByConsumer(
36-
ctx,
37-
ns,
38-
'653860ee26683257394cfe3c'
39-
);
40-
o(serviceAccess);
63+
const accessRequestData = {
64+
acceptLegal: false,
65+
additionalDetails: '',
66+
//applicationId: '5', // App2
67+
controls: '{"clientGenCertificate":false,"jwksUrl":"","clientCertificate":""}',
68+
name: 'Sample API FOR Cope, Aidan CITZ:EX',
69+
productEnvironmentId: '13',
70+
requestor: userId,
71+
} as any;
72+
73+
74+
// userId is needed for Legal
75+
76+
const app = await createApplication(ctx, { name: 'App', description: 'App Desc', ownerId: userId });
77+
78+
accessRequestData.applicationId = app.id;
79+
80+
const result = await addAccessRequest(ctx, accessRequestData);
81+
o(result);
82+
83+
const creds = await collectCredentials(ctx, result.id);
84+
o(creds);
85+
86+
// const serviceAccess = await getOpenAccessRequestsByConsumer(
87+
// ctx,
88+
// ns,
89+
// '653860ee26683257394cfe3c'
90+
// );
91+
// o(serviceAccess);
92+
4193

4294
await keystone.disconnect();
4395
})();

0 commit comments

Comments
 (0)