enhance(SSO): SAML認証でHTTP-POSTバインディングに対応 (MisskeyIO#531)
This commit is contained in:
parent
27c897d19f
commit
aebe9ae148
|
@ -1,15 +0,0 @@
|
|||
export class SingleSignOn1710416761960 {
|
||||
name = 'SingleSignOn1710416761960'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TYPE "public"."sso_service_provider_type_enum" AS ENUM('saml', 'jwt')`);
|
||||
await queryRunner.query(`CREATE TABLE "sso_service_provider" ("id" character varying(36) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "name" character varying(256), "type" "public"."sso_service_provider_type_enum" NOT NULL, "issuer" character varying(512) NOT NULL, "audience" character varying(512) array NOT NULL DEFAULT '{}', "acsUrl" character varying(512) NOT NULL, "publicKey" character varying(4096) NOT NULL, "privateKey" character varying(4096), "signatureAlgorithm" character varying(100) NOT NULL, "cipherAlgorithm" character varying(100), "wantAuthnRequestsSigned" boolean NOT NULL DEFAULT false, "wantAssertionsSigned" boolean NOT NULL DEFAULT true, CONSTRAINT "PK_0e5fff64534026e48e1c248991a" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_86eee7fa4ae68e4a558dc50961" ON "sso_service_provider" ("createdAt") `);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_86eee7fa4ae68e4a558dc50961"`);
|
||||
await queryRunner.query(`DROP TABLE "sso_service_provider"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."sso_service_provider_type_enum"`);
|
||||
}
|
||||
}
|
21
packages/backend/migration/1710667213868-single-sign-on.js
Normal file
21
packages/backend/migration/1710667213868-single-sign-on.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
export class SingleSignOn1710667213868 {
|
||||
name = 'SingleSignOn1710667213868'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS "sso_service_provider"`);
|
||||
await queryRunner.query(`DROP INDEX IF EXISTS "public"."IDX_86eee7fa4ae68e4a558dc50961"`);
|
||||
await queryRunner.query(`DROP TYPE IF EXISTS "public"."sso_service_provider_binding_enum"`);
|
||||
await queryRunner.query(`DROP TYPE IF EXISTS "public"."sso_service_provider_type_enum"`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."sso_service_provider_type_enum" AS ENUM('saml', 'jwt')`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."sso_service_provider_binding_enum" AS ENUM('post', 'redirect')`);
|
||||
await queryRunner.query(`CREATE TABLE "sso_service_provider" ("id" character varying(36) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "name" character varying(256), "type" "public"."sso_service_provider_type_enum" NOT NULL, "issuer" character varying(512) NOT NULL, "audience" character varying(512) array NOT NULL DEFAULT '{}', "binding" "public"."sso_service_provider_binding_enum" NOT NULL, "acsUrl" character varying(512) NOT NULL, "publicKey" character varying(4096) NOT NULL, "privateKey" character varying(4096), "signatureAlgorithm" character varying(100) NOT NULL, "cipherAlgorithm" character varying(100), "wantAuthnRequestsSigned" boolean NOT NULL DEFAULT false, "wantAssertionsSigned" boolean NOT NULL DEFAULT true, CONSTRAINT "PK_0e5fff64534026e48e1c248991a" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_86eee7fa4ae68e4a558dc50961" ON "sso_service_provider" ("createdAt") `);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_86eee7fa4ae68e4a558dc50961"`);
|
||||
await queryRunner.query(`DROP TABLE "sso_service_provider"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."sso_service_provider_binding_enum"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."sso_service_provider_type_enum"`);
|
||||
}
|
||||
}
|
|
@ -39,6 +39,12 @@ export class MiSingleSignOnServiceProvider {
|
|||
})
|
||||
public audience: string[];
|
||||
|
||||
@Column('enum', {
|
||||
enum: ['post', 'redirect'],
|
||||
nullable: false,
|
||||
})
|
||||
public binding: 'post' | 'redirect';
|
||||
|
||||
@Column('varchar', {
|
||||
length: 512,
|
||||
})
|
||||
|
|
|
@ -8,7 +8,7 @@ export const meta = {
|
|||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireAdmin: true,
|
||||
kind: 'write:admin:indie-auth',
|
||||
|
||||
res: {
|
||||
|
|
|
@ -9,7 +9,7 @@ export const meta = {
|
|||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireAdmin: true,
|
||||
kind: 'write:admin:indie-auth',
|
||||
|
||||
errors: {
|
||||
|
|
|
@ -7,7 +7,7 @@ export const meta = {
|
|||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireAdmin: true,
|
||||
kind: 'read:admin:indie-auth',
|
||||
|
||||
res: {
|
||||
|
|
|
@ -9,7 +9,7 @@ export const meta = {
|
|||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireAdmin: true,
|
||||
kind: 'write:admin:indie-auth',
|
||||
|
||||
errors: {
|
||||
|
|
|
@ -11,7 +11,7 @@ export const meta = {
|
|||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireAdmin: true,
|
||||
kind: 'write:admin:sso',
|
||||
|
||||
errors: {
|
||||
|
@ -53,6 +53,11 @@ export const meta = {
|
|||
optional: false, nullable: false,
|
||||
items: { type: 'string', nullable: false },
|
||||
},
|
||||
binding: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
enum: ['post', 'redirect'],
|
||||
},
|
||||
acsUrl: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
|
@ -88,6 +93,7 @@ export const paramDef = {
|
|||
type: { type: 'string', enum: ['saml', 'jwt'], nullable: false },
|
||||
issuer: { type: 'string', nullable: false },
|
||||
audience: { type: 'array', items: { type: 'string', nullable: false }, default: [] },
|
||||
binding: { type: 'string', enum: ['post', 'redirect'], nullable: false },
|
||||
acsUrl: { type: 'string', nullable: false },
|
||||
signatureAlgorithm: { type: 'string', nullable: false },
|
||||
cipherAlgorithm: { type: 'string', nullable: true },
|
||||
|
@ -126,6 +132,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
type: ps.type,
|
||||
issuer: ps.issuer,
|
||||
audience: ps.audience?.filter(i => i.length > 0) ?? [],
|
||||
binding: ps.binding,
|
||||
acsUrl: ps.acsUrl,
|
||||
publicKey: publicKey,
|
||||
privateKey: privateKey,
|
||||
|
@ -147,6 +154,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
type: ssoServiceProvider.type,
|
||||
issuer: ssoServiceProvider.issuer,
|
||||
audience: ssoServiceProvider.audience,
|
||||
binding: ssoServiceProvider.binding,
|
||||
acsUrl: ssoServiceProvider.acsUrl,
|
||||
publicKey: ssoServiceProvider.publicKey,
|
||||
signatureAlgorithm: ssoServiceProvider.signatureAlgorithm,
|
||||
|
|
|
@ -9,7 +9,7 @@ export const meta = {
|
|||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireAdmin: true,
|
||||
kind: 'write:admin:sso',
|
||||
|
||||
errors: {
|
||||
|
|
|
@ -7,7 +7,7 @@ export const meta = {
|
|||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireAdmin: true,
|
||||
kind: 'read:admin:sso',
|
||||
|
||||
res: {
|
||||
|
@ -44,6 +44,11 @@ export const meta = {
|
|||
optional: false, nullable: false,
|
||||
items: { type: 'string', nullable: false },
|
||||
},
|
||||
binding: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
enum: ['post', 'redirect'],
|
||||
},
|
||||
acsUrl: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
|
@ -103,6 +108,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
type: service.type,
|
||||
issuer: service.issuer,
|
||||
audience: service.audience,
|
||||
binding: service.binding,
|
||||
acsUrl: service.acsUrl,
|
||||
useCertificate: service.privateKey != null,
|
||||
publicKey: service.publicKey,
|
||||
|
|
|
@ -10,7 +10,7 @@ export const meta = {
|
|||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireAdmin: true,
|
||||
kind: 'write:admin:sso',
|
||||
|
||||
errors: {
|
||||
|
@ -29,6 +29,7 @@ export const paramDef = {
|
|||
name: { type: 'string', nullable: true },
|
||||
issuer: { type: 'string', nullable: false },
|
||||
audience: { type: 'array', items: { type: 'string', nullable: false } },
|
||||
binding: { type: 'string', enum: ['post', 'redirect'], nullable: false },
|
||||
acsUrl: { type: 'string', nullable: false },
|
||||
signatureAlgorithm: { type: 'string', nullable: false },
|
||||
cipherAlgorithm: { type: 'string', nullable: true },
|
||||
|
@ -65,6 +66,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
name: ps.name !== '' ? ps.name : null,
|
||||
issuer: ps.issuer,
|
||||
audience: ps.audience?.filter(i => i.length > 0),
|
||||
binding: ps.binding,
|
||||
acsUrl: ps.acsUrl,
|
||||
publicKey: publicKey,
|
||||
privateKey: privateKey,
|
||||
|
|
|
@ -104,16 +104,10 @@ export class JWTIdentifyProviderService {
|
|||
});
|
||||
|
||||
fastify.post<{
|
||||
Body: { transaction_id: string; login_token: string; cancel?: string };
|
||||
Body: { transaction_id: string; login_token: string; };
|
||||
}>('/authorize', async (request, reply) => {
|
||||
const transactionId = request.body.transaction_id;
|
||||
const token = request.body.login_token;
|
||||
const cancel = !!request.body.cancel;
|
||||
|
||||
if (cancel) {
|
||||
reply.redirect('/');
|
||||
return;
|
||||
}
|
||||
|
||||
const transaction = await this.redisClient.get(`sso:jwt:transaction:${transactionId}`);
|
||||
if (!transaction) {
|
||||
|
@ -190,13 +184,14 @@ export class JWTIdentifyProviderService {
|
|||
roles: roles.filter(r => r.isPublic).map(r => r.id),
|
||||
};
|
||||
|
||||
let jwt: string;
|
||||
try {
|
||||
if (ssoServiceProvider.cipherAlgorithm) {
|
||||
const key = ssoServiceProvider.publicKey.startsWith('{')
|
||||
? await jose.importJWK(JSON.parse(ssoServiceProvider.publicKey))
|
||||
: jose.base64url.decode(ssoServiceProvider.publicKey);
|
||||
|
||||
const jwt = await new jose.EncryptJWT(payload)
|
||||
jwt = await new jose.EncryptJWT(payload)
|
||||
.setProtectedHeader({
|
||||
alg: ssoServiceProvider.signatureAlgorithm,
|
||||
enc: ssoServiceProvider.cipherAlgorithm,
|
||||
|
@ -208,31 +203,12 @@ export class JWTIdentifyProviderService {
|
|||
.setJti(randomUUID())
|
||||
.setSubject(user.id)
|
||||
.encrypt(key);
|
||||
|
||||
this.#logger.info(`Redirecting to "${ssoServiceProvider.acsUrl}"`, {
|
||||
userId: user.id,
|
||||
ssoServiceProvider: ssoServiceProvider.id,
|
||||
acsUrl: ssoServiceProvider.acsUrl,
|
||||
returnTo,
|
||||
});
|
||||
|
||||
if (returnTo) {
|
||||
reply.redirect(
|
||||
`${ssoServiceProvider.acsUrl}?jwt=${jwt}&return_to=${returnTo}`,
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
reply.redirect(
|
||||
`${ssoServiceProvider.acsUrl}?jwt=${jwt}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const key = ssoServiceProvider.privateKey
|
||||
? await jose.importJWK(JSON.parse(ssoServiceProvider.privateKey))
|
||||
: jose.base64url.decode(ssoServiceProvider.publicKey);
|
||||
|
||||
const jwt = await new jose.SignJWT(payload)
|
||||
jwt = await new jose.SignJWT(payload)
|
||||
.setProtectedHeader({ alg: ssoServiceProvider.signatureAlgorithm })
|
||||
.setIssuer(ssoServiceProvider.issuer)
|
||||
.setAudience(ssoServiceProvider.audience)
|
||||
|
@ -241,25 +217,6 @@ export class JWTIdentifyProviderService {
|
|||
.setJti(randomUUID())
|
||||
.setSubject(user.id)
|
||||
.sign(key);
|
||||
|
||||
this.#logger.info(`Redirecting to "${ssoServiceProvider.acsUrl}"`, {
|
||||
userId: user.id,
|
||||
ssoServiceProvider: ssoServiceProvider.id,
|
||||
acsUrl: ssoServiceProvider.acsUrl,
|
||||
returnTo,
|
||||
});
|
||||
|
||||
if (returnTo) {
|
||||
reply.redirect(
|
||||
`${ssoServiceProvider.acsUrl}?jwt=${jwt}&return_to=${returnTo}`,
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
reply.redirect(
|
||||
`${ssoServiceProvider.acsUrl}?jwt=${jwt}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
this.#logger.error('Failed to create JWT', { error: err });
|
||||
|
@ -289,6 +246,30 @@ export class JWTIdentifyProviderService {
|
|||
} finally {
|
||||
await this.redisClient.del(`sso:jwt:transaction:${transactionId}`);
|
||||
}
|
||||
|
||||
this.#logger.info(`User "${user.username}" authorized for "${ssoServiceProvider.name ?? ssoServiceProvider.issuer}"`);
|
||||
reply.header('Cache-Control', 'no-store');
|
||||
switch (ssoServiceProvider.binding) {
|
||||
case 'post': return reply
|
||||
.status(200)
|
||||
.send({
|
||||
binding: 'post',
|
||||
action: ssoServiceProvider.acsUrl,
|
||||
context: {
|
||||
jwt,
|
||||
return_to: returnTo ?? undefined,
|
||||
},
|
||||
});
|
||||
|
||||
case 'redirect': return reply
|
||||
.status(200)
|
||||
.send({
|
||||
binding: 'redirect',
|
||||
action: !returnTo
|
||||
? `${ssoServiceProvider.acsUrl}?jwt=${jwt}`
|
||||
: `${ssoServiceProvider.acsUrl}?jwt=${jwt}&return_to=${returnTo}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -81,7 +81,7 @@ export class SAMLIdentifyProviderService {
|
|||
const nodes = {
|
||||
'md:EntityDescriptor': {
|
||||
'@xmlns:md': 'urn:oasis:names:tc:SAML:2.0:metadata',
|
||||
'@entityID': provider.issuer,
|
||||
'@entityID': this.config.url,
|
||||
'@validUntil': tenYearsLater,
|
||||
'md:IDPSSODescriptor': {
|
||||
'@WantAuthnRequestsSigned': provider.wantAuthnRequestsSigned,
|
||||
|
@ -105,6 +105,10 @@ export class SAMLIdentifyProviderService {
|
|||
'@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
|
||||
'@Location': `${this.config.url}/sso/saml/${provider.id}`,
|
||||
},
|
||||
{
|
||||
'@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
|
||||
'@Location': `${this.config.url}/sso/saml/${provider.id}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -185,13 +189,14 @@ export class SAMLIdentifyProviderService {
|
|||
'md:NameIDFormat': {
|
||||
'#text': 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
|
||||
},
|
||||
'md:AssertionConsumerService': [
|
||||
{
|
||||
'@index': 1,
|
||||
'@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
|
||||
'@Location': provider.acsUrl,
|
||||
},
|
||||
],
|
||||
'md:AssertionConsumerService': {
|
||||
'@isDefault': 'true',
|
||||
'@index': 0,
|
||||
'@Binding': provider.binding === 'post'
|
||||
? 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
|
||||
: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
|
||||
'@Location': provider.acsUrl,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -235,7 +240,7 @@ export class SAMLIdentifyProviderService {
|
|||
Body?: { SAMLRequest?: string; RelayState?: string };
|
||||
}>('/:serviceId', async (request, reply) => {
|
||||
const serviceId = request.params.serviceId;
|
||||
const binding = 'redirect'; // 今はリダイレクトのみ対応 request.query?.SAMLRequest ? 'redirect' : 'post';
|
||||
const binding = request.query?.SAMLRequest ? 'redirect' : 'post';
|
||||
const samlRequest = request.query?.SAMLRequest ?? request.body?.SAMLRequest;
|
||||
const relayState = request.query?.RelayState ?? request.body?.RelayState;
|
||||
|
||||
|
@ -284,7 +289,6 @@ export class SAMLIdentifyProviderService {
|
|||
`sso:saml:transaction:${transactionId}`,
|
||||
JSON.stringify({
|
||||
serviceId: serviceId,
|
||||
binding: binding,
|
||||
flowResult: parsed,
|
||||
relayState: relayState,
|
||||
}),
|
||||
|
@ -350,16 +354,10 @@ export class SAMLIdentifyProviderService {
|
|||
);
|
||||
|
||||
fastify.post<{
|
||||
Body: { transaction_id: string; login_token: string; cancel?: string };
|
||||
Body: { transaction_id: string; login_token: string; };
|
||||
}>('/authorize', async (request, reply) => {
|
||||
const transactionId = request.body.transaction_id;
|
||||
const token = request.body.login_token;
|
||||
const cancel = !!request.body.cancel;
|
||||
|
||||
if (cancel) {
|
||||
reply.redirect('/');
|
||||
return;
|
||||
}
|
||||
|
||||
const transaction = await this.redisClient.get(`sso:saml:transaction:${transactionId}`);
|
||||
if (!transaction) {
|
||||
|
@ -374,7 +372,7 @@ export class SAMLIdentifyProviderService {
|
|||
return;
|
||||
}
|
||||
|
||||
const { serviceId, binding, flowResult, relayState } = JSON.parse(transaction);
|
||||
const { serviceId, flowResult, relayState } = JSON.parse(transaction);
|
||||
|
||||
const ssoServiceProvider =
|
||||
await this.singleSignOnServiceProviderRepository.findOneBy({ id: serviceId, type: 'saml' });
|
||||
|
@ -439,7 +437,7 @@ export class SAMLIdentifyProviderService {
|
|||
const loginResponse = await idp.createLoginResponse(
|
||||
sp,
|
||||
flowResult,
|
||||
binding,
|
||||
ssoServiceProvider.binding,
|
||||
{},
|
||||
() => {
|
||||
const id = idp.entitySetting.generateID?.() ?? randomUUID();
|
||||
|
@ -655,16 +653,27 @@ export class SAMLIdentifyProviderService {
|
|||
relayState,
|
||||
);
|
||||
|
||||
this.#logger.info(`Redirecting to "${ssoServiceProvider.acsUrl}"`, {
|
||||
userId: user.id,
|
||||
ssoServiceProvider: ssoServiceProvider.id,
|
||||
acsUrl: ssoServiceProvider.acsUrl,
|
||||
relayState: relayState,
|
||||
});
|
||||
|
||||
this.#logger.info(`User "${user.username}" authorized for "${ssoServiceProvider.name ?? ssoServiceProvider.issuer}"`);
|
||||
reply.header('Cache-Control', 'no-store');
|
||||
reply.redirect(loginResponse.context);
|
||||
return;
|
||||
switch (ssoServiceProvider.binding) {
|
||||
case 'post': return reply
|
||||
.status(200)
|
||||
.send({
|
||||
binding: 'post',
|
||||
action: ssoServiceProvider.acsUrl,
|
||||
context: {
|
||||
SAMLResponse: loginResponse.context,
|
||||
RelayState: relayState ?? undefined,
|
||||
},
|
||||
});
|
||||
|
||||
case 'redirect': return reply
|
||||
.status(200)
|
||||
.send({
|
||||
binding: 'redirect',
|
||||
action: loginResponse.context,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
this.#logger.error('Failed to create SAML response', { error: err });
|
||||
const traceableError = err as Error & { code?: string };
|
||||
|
|
|
@ -187,7 +187,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkInput v-model="service.name">
|
||||
<template #label>Name</template>
|
||||
</MkInput>
|
||||
<MkRadios v-model="service.type">
|
||||
<MkRadios v-model="service.type" :disabled="!!service.createdAt">
|
||||
<option value="jwt">JWT</option>
|
||||
<option value="saml">SAML</option>
|
||||
</MkRadios>
|
||||
|
@ -197,6 +197,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkTextarea v-model="service.audience">
|
||||
<template #label>Audience</template>
|
||||
</MkTextarea>
|
||||
<MkRadios v-model="service.binding">
|
||||
<option value="post">POST</option>
|
||||
<option value="redirect">Redirect</option>
|
||||
</MkRadios>
|
||||
<MkInput v-model="service.acsUrl">
|
||||
<template #label>Assertion Consumer Service URL</template>
|
||||
</MkInput>
|
||||
|
@ -426,6 +430,7 @@ function ssoServiceAddNew() {
|
|||
type: 'jwt',
|
||||
issuer: '',
|
||||
audience: '',
|
||||
binding: 'post',
|
||||
acsUrl: '',
|
||||
useCertificate: false,
|
||||
publicKey: '',
|
||||
|
@ -457,6 +462,7 @@ async function ssoServiceSave(service) {
|
|||
type: service.type,
|
||||
issuer: service.issuer,
|
||||
audience: service.audience.split('\n'),
|
||||
binding: service.binding,
|
||||
acsUrl: service.acsUrl,
|
||||
secret: service.publicKey,
|
||||
signatureAlgorithm: service.signatureAlgorithm,
|
||||
|
|
|
@ -7,15 +7,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader/></template>
|
||||
<MkSpacer :contentMax="800">
|
||||
<div v-if="$i">
|
||||
<div v-if="$i && !loading">
|
||||
<div v-if="name">{{ i18n.tsx._auth.shareAccess({ name }) }}</div>
|
||||
<div v-else>{{ i18n.ts._auth.shareAccessAsk }}</div>
|
||||
<form :class="$style.buttons" :action="`/sso/${kind}/authorize`" accept-charset="utf-8" method="post">
|
||||
<input name="transaction_id" class="mk-input-tr-id-hidden" type="hidden" :value="transactionIdMeta?.content"/>
|
||||
<input name="login_token" class="mk-input-token-hidden" type="hidden" :value="$i.token"/>
|
||||
<MkButton inline name="cancel" value="cancel">{{ i18n.ts.cancel }}</MkButton>
|
||||
<MkButton inline primary>{{ i18n.ts.accept }}</MkButton>
|
||||
</form>
|
||||
<div :class="$style.buttons">
|
||||
<MkButton @click="onCancel">{{ i18n.ts.cancel }}</MkButton>
|
||||
<MkButton primary @click="onAccept">{{ i18n.ts.accept }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="$i && loading">
|
||||
<div>{{ i18n.ts._auth.callback }}</div>
|
||||
<MkLoading class="loading"/>
|
||||
<div style="display: none">
|
||||
<form ref="postBindingForm" method="post" :action="actionUrl" autocomplete="off">
|
||||
<input v-for="(value, key) in actionContext" :key="key" :name="key" :value="value" type="hidden"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p :class="$style.loginMessage">{{ i18n.ts._auth.pleaseLogin }}</p>
|
||||
|
@ -26,24 +33,63 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, nextTick } from 'vue';
|
||||
import MkSignin from '@/components/MkSignin.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { $i, login } from '@/account.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
|
||||
const transactionIdMeta = document.querySelector<HTMLMetaElement>('meta[name="misskey:sso:transaction-id"]');
|
||||
if (transactionIdMeta) {
|
||||
transactionIdMeta.remove();
|
||||
}
|
||||
|
||||
const name = document.querySelector<HTMLMetaElement>('meta[name="misskey:sso:service-name"]')?.content;
|
||||
const kind = document.querySelector<HTMLMetaElement>('meta[name="misskey:sso:kind"]')?.content;
|
||||
|
||||
const loading = ref(false);
|
||||
const postBindingForm = ref<HTMLFormElement | null>(null);
|
||||
const actionUrl = ref<string | undefined>(undefined);
|
||||
const actionContext = ref<Record<string, string> | null>(null);
|
||||
|
||||
function onLogin(res): void {
|
||||
login(res.i);
|
||||
}
|
||||
|
||||
function onCancel(): void {
|
||||
if (history.length > 1) history.back();
|
||||
else location.href = '/';
|
||||
}
|
||||
|
||||
function onAccept(): void {
|
||||
loading.value = true;
|
||||
os.promiseDialog(authorize());
|
||||
}
|
||||
|
||||
async function authorize(): Promise<void> {
|
||||
const res = await fetch(`/sso/${kind}/authorize`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
transaction_id: transactionIdMeta?.content,
|
||||
login_token: $i!.token,
|
||||
}),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.binding === 'post') {
|
||||
actionUrl.value = json.action;
|
||||
actionContext.value = json.context;
|
||||
nextTick(() => {
|
||||
postBindingForm.value?.submit();
|
||||
});
|
||||
} else {
|
||||
location.href = json.action;
|
||||
}
|
||||
}
|
||||
|
||||
definePageMetadata(() => ({
|
||||
title: 'Single Sign-On',
|
||||
icon: 'ti ti-apps',
|
||||
|
|
|
@ -10339,6 +10339,8 @@ export type operations = {
|
|||
issuer: string;
|
||||
/** @default [] */
|
||||
audience?: string[];
|
||||
/** @enum {string} */
|
||||
binding?: 'post' | 'redirect';
|
||||
acsUrl: string;
|
||||
signatureAlgorithm: string;
|
||||
cipherAlgorithm?: string | null;
|
||||
|
@ -10365,6 +10367,8 @@ export type operations = {
|
|||
type: 'saml' | 'jwt';
|
||||
issuer: string;
|
||||
audience: string[];
|
||||
/** @enum {string} */
|
||||
binding: 'post' | 'redirect';
|
||||
acsUrl: string;
|
||||
publicKey: string;
|
||||
signatureAlgorithm: string;
|
||||
|
@ -10487,6 +10491,8 @@ export type operations = {
|
|||
type: 'saml' | 'jwt';
|
||||
issuer: string;
|
||||
audience: string[];
|
||||
/** @enum {string} */
|
||||
binding: 'post' | 'redirect';
|
||||
acsUrl: string;
|
||||
useCertificate: boolean;
|
||||
publicKey: string;
|
||||
|
@ -10543,6 +10549,8 @@ export type operations = {
|
|||
name?: string | null;
|
||||
issuer?: string;
|
||||
audience?: string[];
|
||||
/** @enum {string} */
|
||||
binding?: 'post' | 'redirect';
|
||||
acsUrl?: string;
|
||||
signatureAlgorithm?: string;
|
||||
cipherAlgorithm?: string | null;
|
||||
|
|
Loading…
Reference in a new issue