Skip to content

Commit

Permalink
feat: finished up login with identity oidc
Browse files Browse the repository at this point in the history
  • Loading branch information
sheensantoscapadngan committed Jul 3, 2024
1 parent 08322f4 commit 2153dd9
Show file tree
Hide file tree
Showing 8 changed files with 277 additions and 12 deletions.
57 changes: 57 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
"jmespath": "^0.16.0",
"jsonwebtoken": "^9.0.2",
"jsrp": "^0.2.4",
"jwks-rsa": "^3.1.0",
"knex": "^3.0.1",
"ldapjs": "^3.0.7",
"libsodium-wrappers": "^0.7.13",
Expand Down
58 changes: 55 additions & 3 deletions backend/src/server/routes/v1/identity-oidc-auth-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { validateOidcAuthAudiencesField } from "@app/services/identity-oidc-auth/identity-oidc-auth-validators";
import {
validateOidcAuthAudiencesField,
validateOidcBoundClaimsField
} from "@app/services/identity-oidc-auth/identity-oidc-auth-validators";

const IdentityOidcAuthResponseSchema = IdentityOidcAuthsSchema.omit({
encryptedCaCert: true,
Expand All @@ -17,6 +20,55 @@ const IdentityOidcAuthResponseSchema = IdentityOidcAuthsSchema.omit({
});

export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/oidc-auth/login",
config: {
rateLimit: writeLimit
},
schema: {
description: "Login with OIDC Auth",
body: z.object({
identityId: z.string().trim(),
jwt: z.string().trim()
}),
response: {
200: z.object({
accessToken: z.string(),
expiresIn: z.coerce.number(),
accessTokenMaxTTL: z.coerce.number(),
tokenType: z.literal("Bearer")
})
}
},
handler: async (req) => {
const { identityOidcAuth, accessToken, identityAccessToken, identityMembershipOrg } =
await server.services.identityOidcAuth.login({
identityId: req.body.identityId,
jwt: req.body.jwt
});

await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityMembershipOrg?.orgId,
event: {
type: EventType.LOGIN_IDENTITY_OIDC_AUTH,
metadata: {
identityId: identityOidcAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
identityOidcAuthId: identityOidcAuth.id
}
}
});
return {
accessToken,
tokenType: "Bearer" as const,
expiresIn: identityOidcAuth.accessTokenTTL,
accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL
};
}
});

server.route({
method: "POST",
url: "/oidc-auth/identities/:identityId",
Expand Down Expand Up @@ -62,7 +114,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
caCert: z.string().trim().default(""),
boundIssuer: z.string().min(1),
boundAudiences: validateOidcAuthAudiencesField,
boundClaims: z.record(z.string()),
boundClaims: validateOidcBoundClaimsField,
boundSubject: z.string().optional().default("")
}),
response: {
Expand Down Expand Up @@ -154,7 +206,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
caCert: z.string().trim().default(""),
boundIssuer: z.string().min(1),
boundAudiences: validateOidcAuthAudiencesField,
boundClaims: z.record(z.string()),
boundClaims: validateOidcBoundClaimsField,
boundSubject: z.string().optional().default("")
})
.partial(),
Expand Down
136 changes: 133 additions & 3 deletions backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { ForbiddenError } from "@casl/ability";
import axios from "axios";
import https from "https";
import jwt from "jsonwebtoken";
import { JwksClient } from "jwks-rsa";

import { IdentityAuthMethod, SecretKeyEncoding, TIdentityOidcAuthsUpdate } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { getConfig } from "@app/lib/config/env";
import { generateAsymmetricKeyPair } from "@app/lib/crypto";
import {
decryptSymmetric,
Expand All @@ -12,15 +17,17 @@ import {
infisicalSymmetricDecrypt,
infisicalSymmetricEncypt
} from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";

import { AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
import { TOrgBotDALFactory } from "../org/org-bot-dal";
import { TIdentityOidcAuthDALFactory } from "./identity-oidc-auth-dal";
import { TAttachOidcAuthDTO, TGetOidcAuthDTO, TUpdateOidcAuthDTO } from "./identity-oidc-auth-types";
import { TAttachOidcAuthDTO, TGetOidcAuthDTO, TLoginOidcAuthDTO, TUpdateOidcAuthDTO } from "./identity-oidc-auth-types";

type TIdentityOidcAuthServiceFactoryDep = {
identityOidcAuthDAL: TIdentityOidcAuthDALFactory;
Expand All @@ -40,8 +47,130 @@ export const identityOidcAuthServiceFactory = ({
identityDAL,
permissionService,
licenseService,
identityAccessTokenDAL,
orgBotDAL
}: TIdentityOidcAuthServiceFactoryDep) => {
const login = async ({ identityId, jwt: oidcJwt }: TLoginOidcAuthDTO) => {
const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId });
if (!identityOidcAuth) {
throw new UnauthorizedError();
}

const identityMembershipOrg = await identityOrgMembershipDAL.findOne({
identityId: identityOidcAuth.identityId
});
if (!identityMembershipOrg) {
throw new BadRequestError({ message: "Failed to find identity" });
}

const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId });
if (!orgBot) {
throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
}

const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV,
tag: orgBot.symmetricKeyTag,
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
});

const { encryptedCaCert, caCertIV, caCertTag } = identityOidcAuth;

let caCert = "";
if (encryptedCaCert && caCertIV && caCertTag) {
caCert = decryptSymmetric({
ciphertext: encryptedCaCert,
iv: caCertIV,
tag: caCertTag,
key
});
}

const requestAgent = new https.Agent({ ca: caCert, rejectUnauthorized: !!caCert });
const { data: discoveryDoc } = await axios.get<{ jwks_uri: string }>(
`${identityOidcAuth.oidcDiscoveryUrl}/.well-known/openid-configuration`,
{
httpsAgent: requestAgent
}
);
const jwksUri = discoveryDoc.jwks_uri;

const decodedToken = jwt.decode(oidcJwt, { complete: true });
if (!decodedToken) {
throw new BadRequestError({
message: "Invalid JWT"
});
}

const client = new JwksClient({
jwksUri,
requestAgent
});

const { kid } = decodedToken.header;
const oidcSigningKey = await client.getSigningKey(kid);

const tokenData = jwt.verify(oidcJwt, oidcSigningKey.getPublicKey(), {
issuer: identityOidcAuth.boundIssuer
}) as Record<string, string>;

if (identityOidcAuth.boundSubject) {
if (tokenData.sub !== identityOidcAuth.boundSubject) {
throw new UnauthorizedError();
}
}

if (identityOidcAuth.boundAudiences) {
if (!identityOidcAuth.boundAudiences.split(", ").includes(tokenData.aud)) {
throw new UnauthorizedError();
}
}

if (identityOidcAuth.boundClaims) {
Object.keys(identityOidcAuth.boundClaims).forEach((claimKey) => {
const claimValue = (identityOidcAuth.boundClaims as Record<string, string>)[claimKey];
// handle both single and multi-valued claims
if (!claimValue.split(", ").some((claimEntry) => tokenData[claimKey] === claimEntry)) {
throw new UnauthorizedError();
}
});
}

const identityAccessToken = await identityOidcAuthDAL.transaction(async (tx) => {
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityOidcAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityOidcAuth.accessTokenTTL,
accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityOidcAuth.accessTokenNumUsesLimit
},
tx
);
return newToken;
});

const appCfg = getConfig();
const accessToken = jwt.sign(
{
identityId: identityOidcAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
{
expiresIn:
Number(identityAccessToken.accessTokenMaxTTL) === 0
? undefined
: Number(identityAccessToken.accessTokenMaxTTL)
}
);

return { accessToken, identityOidcAuth, identityAccessToken, identityMembershipOrg };
};

const attachOidcAuth = async ({
identityId,
oidcDiscoveryUrl,
Expand Down Expand Up @@ -345,6 +474,7 @@ export const identityOidcAuthServiceFactory = ({
return {
attachOidcAuth,
updateOidcAuth,
getOidcAuth
getOidcAuth,
login
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,8 @@ export type TUpdateOidcAuthDTO = {
export type TGetOidcAuthDTO = {
identityId: string;
} & Omit<TProjectPermission, "projectId">;

export type TLoginOidcAuthDTO = {
identityId: string;
jwt: string;
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,15 @@ export const validateOidcAuthAudiencesField = z
.map((id) => id.trim())
.join(", ");
});

export const validateOidcBoundClaimsField = z.record(z.string()).transform((data) => {
const formattedClaims: Record<string, string> = {};
Object.keys(data).forEach((key) => {
formattedClaims[key] = data[key]
.split(",")
.map((id) => id.trim())
.join(", ");
});

return formattedClaims;
});
Loading

0 comments on commit 2153dd9

Please sign in to comment.