Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
616 changes: 616 additions & 0 deletions MICROSOFT_ACTIVE_DIRECTORY_OAUTH_FLOW.md

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion src/constants/error-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const ERROR_MESSAGES = {
ACCESS_DENIED: 'Access denied',
INVALID_USER_PROFILE: 'Invalid user profile',
GOOGLE_OAUTH_PROFILE_FETCH_FAILED: 'Failed to fetch user profile from Google OAuth service',
MICROSOFT_ACTIVE_DIRECTORY_OAUTH_PROFILE_FETCH_FAILED: 'Failed to fetch user profile from Microsoft Active Directory OAuth service',
LOGOUT_FAILED: 'Logout failed due to an unexpected error.',

INVALID_CREDENTIALS: 'Invalid credentials',
Expand Down Expand Up @@ -130,4 +131,4 @@ export const ERROR_MESSAGES = {
`You do not have permission to import ${model ?? 'these'} records.`,
} as Record<string, (model?: string) => string>,

};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import {
Controller,
Get,
InternalServerErrorException,
Query,
Req,
Res,
UseGuards,
} from "@nestjs/common";
import { ApiQuery, ApiTags } from "@nestjs/swagger";
import { Request, Response } from "express";
import {
DEFAULT_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_TENANT,
MicrosoftActiveDirectoryAuthConfiguration,
isMicrosoftActiveDirectoryOAuthConfigured,
} from "../helpers/microsoft-active-directory-oauth.helper";
import { AuthenticationService } from "../services/authentication.service";
import { SettingService } from "../services/setting.service";
import { Public } from "src/decorators/public.decorator";
import { Auth } from "../decorators/auth.decorator";
import { AuthType } from "../enums/auth-type.enum";
import { MicrosoftActiveDirectoryOauthGuard } from "../passport-strategies/microsoft-active-directory-oauth.strategy";
import { UserService } from "../services/user.service";
import type { SolidCoreSetting } from "../services/settings/default-settings-provider.service";

@Auth(AuthType.None)
@ApiTags("Solid Core")
@Controller("iam/microsoft-active-directory")
export class MicrosoftActiveDirectoryAuthenticationController {
constructor(
private readonly userService: UserService,
private readonly authService: AuthenticationService,
private readonly settingService: SettingService,
) {}

private async getConfiguration(): Promise<MicrosoftActiveDirectoryAuthConfiguration> {
return {
clientID: this.settingService.getConfigValue<SolidCoreSetting>("MICROSOFT_ACTIVE_DIRECTORY_CLIENT_ID"),
clientSecret: this.settingService.getConfigValue<SolidCoreSetting>("MICROSOFT_ACTIVE_DIRECTORY_CLIENT_SECRET"),
tenant: this.settingService.getConfigValue<SolidCoreSetting>("MICROSOFT_ACTIVE_DIRECTORY_TENANT_ID" ) ?? DEFAULT_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_TENANT,
callbackURL: this.settingService.getConfigValue<SolidCoreSetting>("MICROSOFT_ACTIVE_DIRECTORY_CALLBACK_URL"),
redirectURL: this.settingService.getConfigValue<SolidCoreSetting>("MICROSOFT_ACTIVE_DIRECTORY_REDIRECT_URL"),
};
}

private buildFrontendRedirectUrl(redirectURL: string, accessCode: string) {
const separator = redirectURL.includes("?") ? "&" : "?";
return `${redirectURL}${separator}accessCode=${encodeURIComponent(accessCode)}`;
}

private async validateConfiguration() {
const config = await this.getConfiguration();
if (!isMicrosoftActiveDirectoryOAuthConfigured(config)) {
throw new InternalServerErrorException("Microsoft Active Directory OAuth is not configured");
}
return config;
}

@Public()
@UseGuards(MicrosoftActiveDirectoryOauthGuard)
@Get("connect")
async connect() {
await this.validateConfiguration();
}

@Public()
@Get("connect/callback")
@UseGuards(MicrosoftActiveDirectoryOauthGuard)
async microsoftActiveDirectoryAuthCallback(
@Req() req: Request,
@Res() res: Response,
) {
const config = await this.validateConfiguration();
const user = req.user;
return res.redirect(
this.buildFrontendRedirectUrl(config.redirectURL, user["accessCode"]),
);
}

@Public()
@Get("dummy-redirect")
async dummyMicrosoftActiveDirectoryAuthRedirect(
@Query("accessCode") accessCode,
) {
await this.validateConfiguration();
const user = await this.userService.findOneByAccessCode(accessCode);

if (user) {
delete user["password"];
}

return user;
}

@Public()
@Get("authenticate")
@ApiQuery({ name: "accessCode", required: true, type: String })
async microsoftActiveDirectoryAuth(
@Query("accessCode") accessCode: string,
) {
await this.validateConfiguration();
return this.authService.signInUsingMicrosoftActiveDirectory(accessCode);
}
}
14 changes: 13 additions & 1 deletion src/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,18 @@ export class User extends CommonEntity {
// don't send to client
microsoftProfilePicture: string;

@Column({ type: "varchar", nullable: true })
// don't send to client
microsoftActiveDirectoryId: string;

@Column({ type: "varchar", nullable: true })
// don't send to client
microsoftActiveDirectoryAccessToken: string;

@Column({ type: "varchar", nullable: true })
// don't send to client
microsoftActiveDirectoryProfilePicture: string;

@Index()
@Column({ default: true })
@Expose()
Expand Down Expand Up @@ -194,4 +206,4 @@ export class User extends CommonEntity {
@Expose()
apiKeys: UserApiKey[];

}
}
83 changes: 83 additions & 0 deletions src/helpers/microsoft-active-directory-oauth.helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
export type MicrosoftActiveDirectoryAuthConfiguration = {
clientID: string;
clientSecret: string;
tenant: string;
callbackURL: string;
redirectURL: string;
};

export const DEFAULT_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_TENANT = "common";
export const MICROSOFT_ACTIVE_DIRECTORY_OAUTH_SCOPES = [
"openid",
"profile",
"email",
"User.Read",
];

type MicrosoftActiveDirectoryOauthProfileValue = {
value?: string;
};

export type MicrosoftActiveDirectoryOauthProfile = {
id?: string;
displayName?: string;
emails?: MicrosoftActiveDirectoryOauthProfileValue[];
photos?: MicrosoftActiveDirectoryOauthProfileValue[];
_json?: {
id?: string;
displayName?: string;
mail?: string;
userPrincipalName?: string;
email?: string;
preferred_username?: string;
picture?: string;
};
};

export const isMicrosoftActiveDirectoryOAuthConfigured = (
config: MicrosoftActiveDirectoryAuthConfiguration,
): boolean => {
return !!(
config.clientID &&
config.clientSecret &&
config.tenant &&
config.callbackURL &&
config.redirectURL
);
};

export const getMicrosoftActiveDirectoryOAuthProfileId = (
profile: MicrosoftActiveDirectoryOauthProfile,
): string | null => {
return profile.id || profile._json?.id || null;
};

export const getMicrosoftActiveDirectoryOAuthEmail = (
profile: MicrosoftActiveDirectoryOauthProfile,
): string | null => {
const email =
profile.emails?.find((item) => !!item.value)?.value ||
profile._json?.mail ||
profile._json?.userPrincipalName ||
profile._json?.email ||
profile._json?.preferred_username ||
null;

return email?.trim().toLowerCase() || null;
};

export const getMicrosoftActiveDirectoryOAuthDisplayName = (
profile: MicrosoftActiveDirectoryOauthProfile,
): string | null => {
return profile.displayName || profile._json?.displayName || null;
};

export const getMicrosoftActiveDirectoryOAuthPicture = (
profile: MicrosoftActiveDirectoryOauthProfile,
): string | null => {
return (
profile.photos?.find((item) => !!item.value)?.value ||
profile._json?.picture ||
null
);
};
6 changes: 5 additions & 1 deletion src/helpers/user-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export function getUserExcludedFields(): string[] {
"facebookId",
"microsoftAccessToken",
"microsoftId",
"microsoftProfilePicture",
"microsoftActiveDirectoryAccessToken",
"microsoftActiveDirectoryId",
"microsoftActiveDirectoryProfilePicture",
"forgotPasswordConfirmedAt",
"verificationTokenOnForgotPassword",
"verificationTokenOnForgotPasswordExpiresAt",
Expand All @@ -38,4 +42,4 @@ export function getUserExcludedFields(): string[] {
"userViewMetadataIds",
"userViewMetadataCommand",
];
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ export * from './listeners/user-registration.listener'
export * from './passport-strategies/google-oauth.strategy'
export * from './passport-strategies/facebook-oauth.strategy'
export * from './passport-strategies/microsoft-oauth.strategy'
export * from './passport-strategies/microsoft-active-directory-oauth.strategy'

export * from './services/selection-providers/list-of-values-selection-providers.service'

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import { AuthGuard, PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-microsoft';
import { DEFAULT_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_TENANT, MICROSOFT_ACTIVE_DIRECTORY_OAUTH_SCOPES, MicrosoftActiveDirectoryAuthConfiguration, getMicrosoftActiveDirectoryOAuthDisplayName, getMicrosoftActiveDirectoryOAuthEmail, getMicrosoftActiveDirectoryOAuthPicture, getMicrosoftActiveDirectoryOAuthProfileId, isMicrosoftActiveDirectoryOAuthConfigured } from 'src/helpers/microsoft-active-directory-oauth.helper';
import { v4 as uuid } from 'uuid';
import { UserService } from '../services/user.service';

const DUMMY_CLIENT_ID = 'DUMMY_CLIENT_ID';
const DUMMY_CLIENT_SECRET = 'DUMMY_CLIENT_SECRET';
const DUMMY_CALLBACK_URL = 'DUMMY_CALLBACK_URL';

@Injectable()
export class MicrosoftActiveDirectoryOauthGuard extends AuthGuard('microsoft-active-directory') { }


@Injectable()
export class MicrosoftActiveDirectoryOAuthStrategy extends PassportStrategy(Strategy, 'microsoft-active-directory') {
private readonly logger = new Logger(MicrosoftActiveDirectoryOAuthStrategy.name);
constructor(private readonly userService: UserService) {
// TODO: Have added default dummy values for the configuration, since the configuration is not mandatory.
// Perhaps a cleaner way needs to be figured out
const clientID = process.env.IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_CLIENT_ID || DUMMY_CLIENT_ID;
const clientSecret = process.env.IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_CLIENT_SECRET || DUMMY_CLIENT_SECRET;
const tenant = process.env.IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_TENANT_ID || DEFAULT_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_TENANT;
const callbackURL = process.env.IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_CALLBACK_URL || DUMMY_CALLBACK_URL;
const redirectURL = process.env.IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_REDIRECT_URL;

super({ clientID, clientSecret, callbackURL, tenant, scope: MICROSOFT_ACTIVE_DIRECTORY_OAUTH_SCOPES, addUPNAsEmail: true });

const microsoftActiveDirectoryOauth: MicrosoftActiveDirectoryAuthConfiguration = { clientID, clientSecret, tenant, callbackURL, redirectURL }
if (!isMicrosoftActiveDirectoryOAuthConfigured(microsoftActiveDirectoryOauth)) {
this.logger.debug('Microsoft Active Directory OAuth strategy is not configured');
}
}

async validate(_accessToken: string, _refreshToken: string, profile: any, done: any): Promise<any> {
const providerId = getMicrosoftActiveDirectoryOAuthProfileId(profile);

if (!providerId) {
return done(new UnauthorizedException('Microsoft Active Directory OAuth profile is missing an id'), false);
}

// generate a unique access code.
const loginAccessCode: string = uuid();

const user = {
provider: 'microsoftActiveDirectory',
providerId,
email: getMicrosoftActiveDirectoryOAuthEmail(profile),
name: getMicrosoftActiveDirectoryOAuthDisplayName(profile) || getMicrosoftActiveDirectoryOAuthEmail(profile) || 'Microsoft Active Directory User',
picture: getMicrosoftActiveDirectoryOAuthPicture(profile),
accessCode: loginAccessCode,
};

// store the access code and the access token in the database.
// while doing this we also check if the user exists in the database if not we create one.
// if exists then we update the user and store the specified access code & token.
await this.userService.resolveUserOnOauthMicrosoftActiveDirectory({ ...user, accessToken: _accessToken, refreshToken: null });

done(null, user);
}

}
Loading