-
Notifications
You must be signed in to change notification settings - Fork 57
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(api): allow retrieve lti platform registration
Co-authored-by: Nicolas Lepage <[email protected]> Co-authored-by: Vincent Hardouin <[email protected]>
- Loading branch information
1 parent
f79ad4e
commit 90d0366
Showing
7 changed files
with
330 additions
and
0 deletions.
There are no files selected for viewing
78 changes: 78 additions & 0 deletions
78
api/db/database-builder/factory/build-lti-platform-registration.js
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,78 @@ | ||
import { randomUUID, subtle } from 'node:crypto'; | ||
|
||
import { cryptoService } from '../../../src/shared/domain/services/crypto-service.js'; | ||
import { databaseBuffer } from '../database-buffer.js'; | ||
|
||
const defaultKeyPair = await generateJWKPair(); | ||
|
||
export function buildLtiPlatformRegistration({ | ||
clientId = 'AbCD1234', | ||
encryptedPrivateKey = defaultKeyPair.encryptedPrivateKey, | ||
platformOpenIdConfigUrl = 'https://moodle.example.net/mod/lti/openid-configuration.php', | ||
platformOrigin = 'https://moodle.example.net', | ||
publicKey = defaultKeyPair.publicKey, | ||
status = 'active', | ||
toolConfig = { | ||
client_id: 'AbCD1234', | ||
response_types: ['id_token'], | ||
jwks_uri: 'https://pix.example.net/api/lti/keys', | ||
initiate_login_uri: 'https://pix.example.net/api/lti/init', | ||
grant_types: ['client_credentials', 'implicit'], | ||
redirect_uris: ['https://pix.example.net/api/lti/launch'], | ||
application_type: 'web', | ||
token_endpoint_auth_method: 'private_key_jwt', | ||
client_name: 'Pix', | ||
logo_uri: '', | ||
scope: | ||
'https://purl.imsglobal.org/spec/lti-ags/scope/score https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly https://purl.imsglobal.org/spec/lti-ags/scope/lineitem https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly', | ||
'https://purl.imsglobal.org/spec/lti-tool-configuration': { | ||
version: '1.3.0', | ||
deployment_id: '123', | ||
target_link_uri: 'https://pix.example.net/api/lti', | ||
domain: 'pix.example.net', | ||
description: '', | ||
messages: [ | ||
{ | ||
type: 'LtiDeepLinkingRequest', | ||
target_link_uri: 'https://pix.example.net/api/lti/content-selection', | ||
}, | ||
], | ||
claims: ['sub', 'iss', 'name', 'family_name', 'given_name', 'email'], | ||
}, | ||
}, | ||
} = {}) { | ||
return databaseBuffer.pushInsertable({ | ||
tableName: 'lti_platform_registrations', | ||
values: { | ||
clientId, | ||
encryptedPrivateKey, | ||
platformOpenIdConfigUrl, | ||
platformOrigin, | ||
publicKey, | ||
status, | ||
toolConfig, | ||
}, | ||
}); | ||
} | ||
|
||
async function generateJWKPair() { | ||
const keyPair = await subtle.generateKey( | ||
{ | ||
name: 'RSASSA-PKCS1-v1_5', | ||
modulusLength: 4096, | ||
hash: 'SHA-256', | ||
publicExponent: new Uint8Array([1, 0, 1]), | ||
}, | ||
true, | ||
['sign', 'verify'], | ||
); | ||
const privateKey = await subtle.exportKey('jwk', keyPair.privateKey); | ||
const encryptedPrivateKey = await cryptoService.encrypt(JSON.stringify(privateKey)); | ||
const publicKey = await subtle.exportKey('jwk', keyPair.publicKey); | ||
publicKey.kid = randomUUID(); | ||
return { | ||
privateKey, | ||
encryptedPrivateKey, | ||
publicKey, | ||
}; | ||
} |
24 changes: 24 additions & 0 deletions
24
api/db/migrations/20250307144507_fix-lti-platform-registration-encrypted-key-length.js
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,24 @@ | ||
const TABLE_NAME = 'lti_platform_registrations'; | ||
const COLUMN_NAME = 'encryptedPrivateKey'; | ||
|
||
/** | ||
* @param { import("knex").Knex } knex | ||
* @returns { Promise<void> } | ||
*/ | ||
const up = async function (knex) { | ||
await knex.schema.alterTable(TABLE_NAME, function (table) { | ||
table.text(COLUMN_NAME).alter({ alterNullable: false, alterType: true }); | ||
}); | ||
}; | ||
|
||
/** | ||
* @param { import("knex").Knex } knex | ||
* @returns { Promise<void> } | ||
*/ | ||
const down = async function (knex) { | ||
await knex.schema.alterTable(TABLE_NAME, function (table) { | ||
table.string(COLUMN_NAME).alter({ alterNullable: false, alterType: true }); | ||
}); | ||
}; | ||
|
||
export { down, up }; |
11 changes: 11 additions & 0 deletions
11
api/src/identity-access-management/domain/models/LtiPlatformRegistration.js
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,11 @@ | ||
export class LtiPlatformRegistration { | ||
constructor({ clientId, platformOrigin, status, toolConfig, encryptedPrivateKey, publicKey, platformOpenIdConfig }) { | ||
this.clientId = clientId; | ||
this.platformOrigin = platformOrigin; | ||
this.status = status; | ||
this.toolConfig = toolConfig; | ||
this.encryptedPrivateKey = encryptedPrivateKey; | ||
this.publicKey = publicKey; | ||
this.platformOpenIdConfig = platformOpenIdConfig; | ||
} | ||
} |
23 changes: 23 additions & 0 deletions
23
...ity-access-management/infrastructure/repositories/lti-platform-registration.repository.js
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,23 @@ | ||
import { knex } from '../../../../db/knex-database-connection.js'; | ||
import { httpAgent } from '../../../shared/infrastructure/http-agent.js'; | ||
import { LtiPlatformRegistration } from '../../domain/models/LtiPlatformRegistration.js'; | ||
|
||
export const ltiPlatformRegistrationRepository = { | ||
async getByClientId(clientId) { | ||
const ltiPlatformRegistrationDTO = await knex | ||
.select('*') | ||
.from('lti_platform_registrations') | ||
.where('clientId', clientId) | ||
.first(); | ||
|
||
if (!ltiPlatformRegistrationDTO) { | ||
return undefined; | ||
} | ||
|
||
const { data: platformOpenIdConfig } = await httpAgent.get({ | ||
url: ltiPlatformRegistrationDTO.platformOpenIdConfigUrl, | ||
}); | ||
|
||
return new LtiPlatformRegistration({ ...ltiPlatformRegistrationDTO, platformOpenIdConfig }); | ||
}, | ||
}; |
40 changes: 40 additions & 0 deletions
40
...ment/integration/infrastructure/repositories/lti-platform-registration.repository.test.js
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,40 @@ | ||
import { ltiPlatformRegistrationRepository } from '../../../../../src/identity-access-management/infrastructure/repositories/lti-platform-registration.repository.js'; | ||
import { databaseBuilder, domainBuilder, expect, nock } from '../../../../test-helper.js'; | ||
|
||
describe('Integration | Identity Access Management | Infrastructure | Repository | lti-platform-registration', function () { | ||
describe('#getByClientId', function () { | ||
it('should return LTI platform registration information', async function () { | ||
// given | ||
const clientId = 'AbCD1234'; | ||
const expectedLtiPlatformRegistration = domainBuilder.identityAccessManagement.buildLtiPlatformRegistration(); | ||
|
||
const savedLtiPlatformRegistration = databaseBuilder.factory.buildLtiPlatformRegistration( | ||
expectedLtiPlatformRegistration, | ||
); | ||
await databaseBuilder.commit(); | ||
|
||
const platformOpenIdConfigUrl = new URL(savedLtiPlatformRegistration.platformOpenIdConfigUrl); | ||
const platformOpenIdConfigCall = nock(platformOpenIdConfigUrl.origin) | ||
.get(platformOpenIdConfigUrl.pathname) | ||
.reply(200, expectedLtiPlatformRegistration.platformOpenIdConfig); | ||
|
||
// when | ||
const registration = await ltiPlatformRegistrationRepository.getByClientId(clientId); | ||
|
||
// then | ||
expect(platformOpenIdConfigCall.isDone()).to.be.true; | ||
expect(registration).to.deepEqualInstance(expectedLtiPlatformRegistration); | ||
}); | ||
|
||
it('should return undefined when any LTI platform are found', async function () { | ||
// given | ||
const clientId = 'NOT-FOUND'; | ||
|
||
// when | ||
const registration = await ltiPlatformRegistrationRepository.getByClientId(clientId); | ||
|
||
// then | ||
expect(registration).to.be.undefined; | ||
}); | ||
}); | ||
}); |
152 changes: 152 additions & 0 deletions
152
...ling/domain-builder/factory/identity-access-management/build-lti-platform-registration.js
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,152 @@ | ||
import { randomUUID, subtle } from 'node:crypto'; | ||
|
||
import { LtiPlatformRegistration } from '../../../../../src/identity-access-management/domain/models/LtiPlatformRegistration.js'; | ||
import { cryptoService } from '../../../../../src/shared/domain/services/crypto-service.js'; | ||
|
||
const defaultKeyPair = await generateJWKPair(); | ||
|
||
export function buildLtiPlatformRegistration({ | ||
clientId = 'AbCD1234', | ||
encryptedPrivateKey = defaultKeyPair.encryptedPrivateKey, | ||
platformOpenIdConfig = { | ||
issuer: 'https://moodle.example.net', | ||
token_endpoint: 'https://moodle.example.net/mod/lti/token.php', | ||
token_endpoint_auth_methods_supported: ['private_key_jwt'], | ||
token_endpoint_auth_signing_alg_values_supported: ['RS256'], | ||
jwks_uri: 'https://moodle.example.net/mod/lti/certs.php', | ||
authorization_endpoint: 'https://moodle.example.net/mod/lti/auth.php', | ||
registration_endpoint: 'https://moodle.example.net/mod/lti/openid-registration.php', | ||
scopes_supported: [ | ||
'https://purl.imsglobal.org/spec/lti-bo/scope/basicoutcome', | ||
'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly', | ||
'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly', | ||
'https://purl.imsglobal.org/spec/lti-ags/scope/score', | ||
'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem', | ||
'https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly', | ||
'https://purl.imsglobal.org/spec/lti-ts/scope/toolsetting', | ||
'openid', | ||
], | ||
response_types_supported: ['id_token'], | ||
subject_types_supported: ['public', 'pairwise'], | ||
id_token_signing_alg_values_supported: ['RS256'], | ||
claims_supported: ['sub', 'iss', 'name', 'given_name', 'family_name', 'email'], | ||
'https://purl.imsglobal.org/spec/lti-platform-configuration': { | ||
product_family_code: 'moodle', | ||
version: '4.5.2 (Build: 20250210)', | ||
messages_supported: [ | ||
{ | ||
type: 'LtiResourceLinkRequest', | ||
}, | ||
{ | ||
type: 'LtiDeepLinkingRequest', | ||
placements: ['ContentArea'], | ||
}, | ||
], | ||
variables: [ | ||
'basic-lti-launch-request', | ||
'ContentItemSelectionRequest', | ||
'ToolProxyRegistrationRequest', | ||
'Context.id', | ||
'Context.title', | ||
'Context.label', | ||
'Context.id.history', | ||
'Context.sourcedId', | ||
'Context.longDescription', | ||
'Context.timeFrame.begin', | ||
'CourseSection.title', | ||
'CourseSection.label', | ||
'CourseSection.sourcedId', | ||
'CourseSection.longDescription', | ||
'CourseSection.timeFrame.begin', | ||
'CourseSection.timeFrame.end', | ||
'ResourceLink.id', | ||
'ResourceLink.title', | ||
'ResourceLink.description', | ||
'User.id', | ||
'User.username', | ||
'Person.name.full', | ||
'Person.name.given', | ||
'Person.name.family', | ||
'Person.email.primary', | ||
'Person.sourcedId', | ||
'Person.name.middle', | ||
'Person.address.street1', | ||
'Person.address.locality', | ||
'Person.address.country', | ||
'Person.address.timezone', | ||
'Person.phone.primary', | ||
'Person.phone.mobile', | ||
'Person.webaddress', | ||
'Membership.role', | ||
'Result.sourcedId', | ||
'Result.autocreate', | ||
'BasicOutcome.sourcedId', | ||
'BasicOutcome.url', | ||
'Moodle.Person.userGroupIds', | ||
], | ||
}, | ||
}, | ||
platformOrigin = 'https://moodle.example.net', | ||
publicKey = defaultKeyPair.publicKey, | ||
status = 'active', | ||
toolConfig = { | ||
client_id: clientId, | ||
response_types: ['id_token'], | ||
jwks_uri: 'https://pix.example.net/api/lti/keys', | ||
initiate_login_uri: 'https://pix.example.net/api/lti/init', | ||
grant_types: ['client_credentials', 'implicit'], | ||
redirect_uris: ['https://pix.example.net/api/lti/launch'], | ||
application_type: 'web', | ||
token_endpoint_auth_method: 'private_key_jwt', | ||
client_name: 'Pix', | ||
logo_uri: '', | ||
scope: | ||
'https://purl.imsglobal.org/spec/lti-ags/scope/score https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly https://purl.imsglobal.org/spec/lti-ags/scope/lineitem https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly', | ||
'https://purl.imsglobal.org/spec/lti-tool-configuration': { | ||
version: '1.3.0', | ||
deployment_id: '123', | ||
target_link_uri: 'https://pix.example.net/api/lti', | ||
domain: 'pix.example.net', | ||
description: '', | ||
messages: [ | ||
{ | ||
type: 'LtiDeepLinkingRequest', | ||
target_link_uri: 'https://pix.example.net/api/lti/content-selection', | ||
}, | ||
], | ||
claims: ['sub', 'iss', 'name', 'family_name', 'given_name', 'email'], | ||
}, | ||
}, | ||
} = {}) { | ||
return new LtiPlatformRegistration({ | ||
clientId, | ||
encryptedPrivateKey, | ||
platformOpenIdConfig, | ||
platformOrigin, | ||
publicKey, | ||
status, | ||
toolConfig, | ||
}); | ||
} | ||
|
||
async function generateJWKPair() { | ||
const keyPair = await subtle.generateKey( | ||
{ | ||
name: 'RSASSA-PKCS1-v1_5', | ||
modulusLength: 4096, | ||
hash: 'SHA-256', | ||
publicExponent: new Uint8Array([1, 0, 1]), | ||
}, | ||
true, | ||
['sign', 'verify'], | ||
); | ||
const privateKey = await subtle.exportKey('jwk', keyPair.privateKey); | ||
const encryptedPrivateKey = await cryptoService.encrypt(JSON.stringify(privateKey)); | ||
const publicKey = await subtle.exportKey('jwk', keyPair.publicKey); | ||
publicKey.kid = randomUUID(); | ||
return { | ||
privateKey, | ||
encryptedPrivateKey, | ||
publicKey, | ||
}; | ||
} |
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