14 Commits

Author SHA1 Message Date
8734abcc99 updating ms graph module 2026-05-11 20:47:40 -05:00
c66cfb3c9e switching user module to msgraph module (#15) 2025-11-16 09:28:06 -06:00
aa09a2685c adding public decorator (#14) 2025-11-16 09:25:35 -06:00
ea108de9cd 12 change auth to OIDC (#13)
* switching auth strategy to oidc

* switching auth strategy to oidc
2025-08-26 21:11:16 -05:00
7f86bb383e removing public decorator 2025-08-26 12:56:35 -05:00
04d796da6c Upgrading to nest v11 2025-03-08 08:32:45 -06:00
24cde897d5 Updating version number to 1.0.0 2025-02-20 18:28:36 -06:00
361ed27db4 Adding user module and removing msgraph module 2025-02-20 18:27:46 -06:00
30b500bd7f refactoring 2025-02-01 11:07:55 -06:00
533d5d166b updating dynamic module to configurable module builder 2025-01-30 12:46:33 -06:00
41a1b9f807 updating az tables 2024-11-19 20:51:31 -06:00
67014a3080 changing to azure storage connection string (#11) 2024-10-11 18:45:55 -05:00
e192dcdb05 Feature/8 add optional auth guard (#9)
* adding optional auth to guard

* adding optional auth to guard
2024-09-24 08:58:43 -05:00
fcffd65d33 Feature/3 add az table module (#7)
* adding azure table client module

* adding azure table client module

* adding azure table client module

* adding azure table client module

* adding azure table client module
2024-08-11 19:51:23 -05:00
31 changed files with 1913 additions and 1787 deletions

View File

@@ -0,0 +1,4 @@
import { Controller } from '@nestjs/common';
@Controller('auth-controller')
export class AuthControllerController {}

View File

@@ -1,24 +1,22 @@
import { CanActivate } from '@nestjs/common';
import { ExecutionContext, Injectable } from '@nestjs/common'; import { ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core'; import { Reflector } from '@nestjs/core';
import { AuthGuard as PassportAuthGuard } from '@nestjs/passport'; import { AuthGuard as PassportAuthGuard } from '@nestjs/passport';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
@Injectable() @Injectable()
export class AuthGuard extends PassportAuthGuard('azure-ad') implements CanActivate { export class AuthGuard extends PassportAuthGuard('jwt') {
constructor(private readonly reflector: Reflector) { constructor(private readonly reflector: Reflector) {
super(); super();
} }
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> { canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const isPublic = this.reflector.get<boolean>( const isPublic = this.reflector.get<boolean>(
'isPublic', IS_PUBLIC_KEY,
context.getHandler() context.getHandler()
); )
if (isPublic) { if (isPublic) return true;
return true;
}
return super.canActivate(context); return super.canActivate(context);
} }

View File

@@ -1,5 +1,5 @@
export interface AuthOptions { export interface AuthModuleOptions {
tenantId: string; audience: string;
clientId: string; issuerUrl: string;
clientSecret: string; jwksUri: string;
} }

View File

@@ -0,0 +1,4 @@
import { ConfigurableModuleBuilder } from '@nestjs/common';
import { AuthModuleOptions } from './auth.interface';
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } = new ConfigurableModuleBuilder<AuthModuleOptions>().build()

View File

@@ -1,26 +1,16 @@
import { Module, DynamicModule } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AuthOptions } from './auth.interface';
import { AUTH_OPTIONS } from './auth.constants';
import { PassportModule } from '@nestjs/passport'; import { PassportModule } from '@nestjs/passport';
import { AzureAdStrategy } from './auth.strategy'; import { AuthStrategy } from './auth.strategy';
import { ConfigurableModuleClass } from './auth.module-definition';
@Module({}) @Module({
export class AuthModule {
static register(options: AuthOptions): DynamicModule {
return {
module: AuthModule,
imports: [ imports: [
PassportModule.register({ PassportModule.register({
defaultStrategy: 'azure-ad' defaultStrategy: 'jwt'
}) })
], ],
providers: [ providers: [
{ AuthStrategy
provide: AUTH_OPTIONS,
useValue: options
},
AzureAdStrategy
] ]
}; })
} export class AuthModule extends ConfigurableModuleClass {}
}

View File

@@ -1,25 +1,29 @@
import { Inject, Injectable } from "@nestjs/common"; import { Inject, Injectable } from '@nestjs/common';
import { PassportStrategy } from "@nestjs/passport"; import { PassportStrategy } from '@nestjs/passport';
import { BearerStrategy } from 'passport-azure-ad'; import { ExtractJwt, Strategy } from 'passport-jwt';
import { AuthOptions } from './auth.interface' import * as jwksRsa from 'jwks-rsa';
import { AUTH_OPTIONS } from "./auth.constants"; import { AuthModuleOptions } from './auth.interface';
import { MODULE_OPTIONS_TOKEN } from './auth.module-definition';
@Injectable() @Injectable()
export class AzureAdStrategy extends PassportStrategy( export class AuthStrategy extends PassportStrategy(Strategy, 'jwt') {
BearerStrategy, constructor(@Inject(MODULE_OPTIONS_TOKEN) authModuleOptions: AuthModuleOptions) {
'azure-ad'
) {
constructor(@Inject(AUTH_OPTIONS) authOptions: AuthOptions) {
super({ super({
identityMetadata: `https://login.microsoftonline.com/${authOptions.tenantId}/.well-known/openid-configuration`, jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
clientID: authOptions.clientId, audience: authModuleOptions.audience,
audience: `api://${authOptions.clientId}`, issuer: authModuleOptions.issuerUrl,
loggingLevel: 'info', algorithms: ['RS256'],
loggingNoPII: false ignoreExpiration: false,
}) secretOrKeyProvider: jwksRsa.passportJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: authModuleOptions.jwksUri,
}),
});
} }
async validate(data: any): Promise<any> { validate(payload: any) {
return data; return payload;
} }
} }

View File

@@ -1,3 +0,0 @@
import { SetMetadata } from "@nestjs/common";
export const Public = () => SetMetadata('isPublic', true);

View File

@@ -1 +0,0 @@
export const APP_CONFIG_OPTIONS = 'APP_CONFIG_OPTIONS';

View File

@@ -1,21 +0,0 @@
import { Module, DynamicModule } from '@nestjs/common';
import { AppConfigService } from './az-app-config.service';
import { AppConfigOptions } from './az-app-config.interface';
import { APP_CONFIG_OPTIONS } from './az-app-config.constants';
@Module({})
export class AppConfigModule {
static register(options: AppConfigOptions): DynamicModule {
return {
module: AppConfigModule,
providers: [
{
provide: APP_CONFIG_OPTIONS,
useValue: options
},
AppConfigService
],
exports: [AppConfigService]
};
}
}

View File

@@ -1,38 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { AppConfigOptions } from './az-app-config.interface';
import { APP_CONFIG_OPTIONS } from './az-app-config.constants';
import axios, { AxiosResponse } from 'axios';
import { AppConfigurationClient, ConfigurationSetting, FeatureFlagValue, GetConfigurationSettingResponse, featureFlagPrefix, isFeatureFlag, parseFeatureFlag } from '@azure/app-configuration';
import { ClientSecretCredential } from '@azure/identity';
@Injectable()
export class AppConfigService {
constructor(
@Inject(APP_CONFIG_OPTIONS)
private appConfigOptions: AppConfigOptions
) { }
async getFeatureFlags(keys: string[], label: string): Promise<{ key: string, enabled: boolean }[]> {
const credential: ClientSecretCredential = new ClientSecretCredential(this.appConfigOptions.tenantId, this.appConfigOptions.clientId, this.appConfigOptions.clientSecret);
const client: AppConfigurationClient = new AppConfigurationClient(this.appConfigOptions.url, credential);
const featureFlags: { key: string, enabled: boolean }[] = await Promise.all(
keys.map(async (key: string) => {
const configSetting: GetConfigurationSettingResponse = await client.getConfigurationSetting({
key: `${featureFlagPrefix}${key}`,
label: label
});
if (isFeatureFlag(configSetting)) {
const parsedFeatureFlag: ConfigurationSetting<FeatureFlagValue> = parseFeatureFlag(configSetting);
return {
key: parsedFeatureFlag.value.id,
enabled: parsedFeatureFlag.value.enabled
}
}
})
);
return featureFlags;
}
}

View File

@@ -1 +0,0 @@
export const TABLE_OPTIONS = 'TABLE_OPTIONS';

View File

@@ -1,4 +0,0 @@
export interface TableOptions {
accountName: string;
accountKey: string;
}

View File

@@ -1,21 +0,0 @@
import { Module, DynamicModule } from '@nestjs/common';
import { TableService } from './az-table.service';
import { TableOptions } from './az-table.interface';
import { TABLE_OPTIONS } from './az-table.constants';
@Module({})
export class TableModule {
static register(options: TableOptions): DynamicModule {
return {
module: TableModule,
providers: [
{
provide: TABLE_OPTIONS,
useValue: options
},
TableModule
],
exports: [TableService]
}
}
}

View File

@@ -1,19 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { TableOptions } from './az-table.interface';
import { TABLE_OPTIONS } from './az-table.constants';
import { TableClient, AzureNamedKeyCredential } from '@azure/data-tables';
@Injectable()
export class TableService {
constructor(
@Inject(TABLE_OPTIONS)
private tablesOptions: TableOptions
) {}
async getTableClient(tableName: string): Promise<TableClient> {
const credential: AzureNamedKeyCredential = new AzureNamedKeyCredential(this.tablesOptions.accountName, this.tablesOptions.accountKey);
const tableClient: TableClient = new TableClient(`https://${this.tablesOptions.accountName}.table.core.windows.net`, tableName, credential);
return tableClient;
}
}

View File

@@ -0,0 +1,4 @@
import { SetMetadata } from "@nestjs/common";
export const IS_PUBLIC_KEY = "isPublic";
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

View File

@@ -1,21 +1,19 @@
// Azure App Configuration
export * from './azAppConfig/az-app-config.module';
export * from './azAppConfig/az-app-config.service';
// Azure Data Tables
export * from './azTable/az-table.module';
export * from './azTable/az-table.service';
// Auth // Auth
export * from './auth/auth.module'; export * from './auth/auth.module';
export * from './auth/auth.gaurd'; export * from './auth/auth.guard';
export * from './auth/public.decorator';
// Custom Error
export * from './util/customError';
// Decorators
export { IS_PUBLIC_KEY, Public } from './decorators/public.decorator'
// MsGraph
export * from './msgraph/msgraph.module';
export * from './msgraph/msgraph.service';
// MS Graph
export * from './msGraph/ms-graph.module';
export * from './msGraph/ms-graph.service';
export { Client as MsGraphClient } from '@microsoft/microsoft-graph-client';

View File

@@ -1 +0,0 @@
export const MS_GRAPH_OPTIONS = 'MS_GRAPH_OPTIONS';

View File

@@ -1,5 +0,0 @@
export interface MsGraphOptions {
tenantId: string;
clientId: string;
clientSecret: string;
}

View File

@@ -1,21 +0,0 @@
import { Module, DynamicModule } from '@nestjs/common';
import { MsGraphService } from './ms-graph.service';
import { MsGraphOptions } from './ms-graph.interface';
import { MS_GRAPH_OPTIONS } from './ms-graph.constants';
@Module({})
export class MsGraphModule {
static register(options: MsGraphOptions): DynamicModule {
return {
module: MsGraphModule,
providers: [
{
provide: MS_GRAPH_OPTIONS,
useValue: options
},
MsGraphService
],
exports: [MsGraphService]
}
}
}

View File

@@ -1,41 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { MsGraphOptions } from './ms-graph.interface';
import { MS_GRAPH_OPTIONS } from './ms-graph.constants';
import { Client } from '@microsoft/microsoft-graph-client';
import { AuthenticationResult, ConfidentialClientApplication, OnBehalfOfRequest } from '@azure/msal-node';
@Injectable()
export class MsGraphService {
constructor(@Inject(MS_GRAPH_OPTIONS) private msGraphOptions: MsGraphOptions) {}
async getMsGraphAuth(accessToken: string, scopes: string[]): Promise<string> {
try {
const oboRequest: OnBehalfOfRequest = {
oboAssertion: accessToken,
scopes: scopes
}
const cca = new ConfidentialClientApplication({
auth: {
clientId: this.msGraphOptions.clientId,
clientSecret: this.msGraphOptions.clientSecret,
authority: `https://login.microsoftonline.com/${this.msGraphOptions.tenantId}`
}
});
const authenticationResult: AuthenticationResult = await cca.acquireTokenOnBehalfOf(oboRequest);
return authenticationResult.accessToken
} catch (error) {
return error
}
}
async getMsGraphClientDelegated(accessToken): Promise<Client> {
const client = await Client.init({
authProvider: (done) => {
done(null, accessToken);
}
});
return client;
}
}

View File

@@ -0,0 +1 @@
export const MSGRAPH_MODULE_OPTIONS = 'MSGRAPH_MODULE_OPTIONS'

View File

@@ -0,0 +1,76 @@
import {
Controller,
Get,
Headers,
Query,
StreamableFile,
UseGuards,
} from '@nestjs/common';
import { MsGraphService } from './msgraph.service';
import { Client as MsGraphClient } from '@microsoft/microsoft-graph-client';
import { AuthGuard } from '../auth/auth.guard';
@Controller('msgraph')
export class MsGraphController {
constructor(
private readonly msGraphService: MsGraphService
) {}
@Get('photo')
@UseGuards(AuthGuard)
async getProfilePhoto(@Headers() headers: any): Promise<StreamableFile> {
try {
const graphToken: string = await this.msGraphService.getMsGraphAuth(
headers.authorization.replace('Bearer ', ''),
['user.read']
);
const client: MsGraphClient =
await this.msGraphService.getMsGraphClientDelegated(graphToken);
const blob: Blob = await client.api(`me/photos('48x48')/$value`).get();
const arrayBuffer: ArrayBuffer = await blob.arrayBuffer();
const buffer: Buffer = Buffer.from(arrayBuffer);
return new StreamableFile(buffer, {
type: 'application/json',
disposition: `attachment; filename="user_photo.png"`
});
} catch (error) {
return error;
}
}
@Get('profile')
@UseGuards(AuthGuard)
async getUserProfile(@Headers() headers: any): Promise<any> {
try {
const graphToken: string = await this.msGraphService.getMsGraphAuth(
headers.authorization.replace('Bearer ', ''),
['user.read']
);
const client: MsGraphClient =
await this.msGraphService.getMsGraphClientDelegated(graphToken);
const userProfile = await client.api(`me`).get();
return userProfile;
} catch (error) {
return error;
}
}
@Get('search')
@UseGuards(AuthGuard)
async searchUsers(
@Headers() headers: any,
@Query('search') search: any
): Promise<any> {
try {
const accessToken: string = headers.authorization.replace('Bearer ', '');
const personSearchResults: any[] =
await this.msGraphService.searchUsers(accessToken, search);
return personSearchResults;
} catch (error) {
return error;
}
}
}

View File

@@ -1,6 +1,6 @@
export interface AppConfigOptions { export interface MsGraphModuleOptions {
url: string; authority: string;
tenantId: string;
clientId: string; clientId: string;
clientSecret: string; clientSecret: string;
tenantId: string;
} }

View File

@@ -0,0 +1,4 @@
import { ConfigurableModuleBuilder } from '@nestjs/common';
import { MsGraphModuleOptions } from './msgraph.interface';
export const { ConfigurableModuleClass, ASYNC_OPTIONS_TYPE, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE } = new ConfigurableModuleBuilder<MsGraphModuleOptions>().build()

View File

@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { MsGraphService } from './msgraph.service';
import { ConfigurableModuleClass } from './msgraph.module-definition';
import { MsGraphController } from './msgraph.controller';
@Module({
controllers: [MsGraphController],
providers: [MsGraphService],
exports: [MsGraphService]
})
export class MsGraphModule extends ConfigurableModuleClass {}

View File

@@ -0,0 +1,96 @@
import { Inject, Injectable, StreamableFile } from '@nestjs/common';
import { MsGraphModuleOptions } from './msgraph.interface';
import { Client } from '@microsoft/microsoft-graph-client';
import { AuthenticationResult, ConfidentialClientApplication, OnBehalfOfRequest } from '@azure/msal-node';
import { MODULE_OPTIONS_TOKEN } from './msgraph.module-definition';
@Injectable()
export class MsGraphService {
constructor(@Inject(MODULE_OPTIONS_TOKEN) private msGraphModuleOptions: MsGraphModuleOptions) {}
async getMsGraphAuth(accessToken: string, scopes: string[]): Promise<string> {
try {
const oboRequest: OnBehalfOfRequest = {
oboAssertion: accessToken,
scopes: scopes
}
const cca = new ConfidentialClientApplication({
auth: {
clientId: this.msGraphModuleOptions.clientId,
clientSecret: this.msGraphModuleOptions.clientSecret,
authority: this.msGraphModuleOptions.authority
}
});
const authenticationResult: AuthenticationResult = await cca.acquireTokenOnBehalfOf(oboRequest);
return authenticationResult.accessToken
} catch (error) {
return error
}
}
async getMsGraphClientDelegated(accessToken): Promise<Client> {
const client = await Client.init({
authProvider: (done) => {
done(null, accessToken);
}
});
return client;
}
async getProfilePhoto(accessToken: string, scopes: string[]): Promise<Buffer> {
try {
const graphToken: string = await this.getMsGraphAuth(accessToken, scopes);
const client: Client = await this.getMsGraphClientDelegated(graphToken);
const blob: Blob = await client.api(`me/photos('48x48)/$value`).get();
const arrayBuffer: ArrayBuffer = await blob.arrayBuffer();
const buffer: Buffer = Buffer.from(arrayBuffer);
return buffer;
} catch (error) {
return error;
}
}
async getUserProfile(accessToken): Promise<any> {
try {
const graphToken: string = await this.getMsGraphAuth(accessToken, ['user.read']);
const client: Client = await this.getMsGraphClientDelegated(graphToken);
const userProfile = await client.api(`me`).get();
return userProfile
} catch (error) {
return error;
}
}
async searchUsers(accessToken, searchText: string): Promise<any> {
try {
const graphToken: string = await this.getMsGraphAuth(accessToken, ['user.read.all']);
const client: Client = await this.getMsGraphClientDelegated(graphToken);
const results: any = await client
.api('users')
.header('ConsistencyLevel', 'eventual')
.search(`"displayName:${searchText}"`)
.orderby('displayName')
.select(['displayName', 'userPrincipalName'])
.get();
let personResults: any[];
if (results.value) {
personResults = results.value.filter((result: any) => {
if (result.userPrincipalName !== null) {
return result;
}
});
} else {
personResults = []
}
return personResults;
} catch (error) {
throw new Error(error);
}
}
}

View File

@@ -0,0 +1,9 @@
export class CustomError extends Error {
statusCode: number;
constructor(message, name, statusCode) {
super(message);
this.name = name;
this.statusCode = statusCode;
}
}

View File

@@ -5,5 +5,5 @@
"outDir": "../../dist/libs/modules" "outDir": "../../dist/libs/modules"
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"] "exclude": ["node_modules", "src/**/*.spec.ts"]
} }

3147
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "@noahspan/noahspan-modules", "name": "@noahspan/noahspan-modules",
"version": "0.3.8", "version": "1.2.11",
"description": "", "description": "",
"author": "", "author": "",
"license": "UNLICENSED", "license": "UNLICENSED",
@@ -24,28 +24,32 @@
"test:e2e": "jest --config ./test/jest-e2e.json" "test:e2e": "jest --config ./test/jest-e2e.json"
}, },
"dependencies": { "dependencies": {
"@azure/app-configuration": "^1.6.0",
"@azure/data-tables": "^13.2.2",
"@azure/identity": "^4.2.0", "@azure/identity": "^4.2.0",
"@azure/msal-node": "^2.9.2", "@azure/msal-node": "^2.9.2",
"@microsoft/microsoft-graph-client": "^3.0.7", "@microsoft/microsoft-graph-client": "^3.0.7",
"@nestjs/common": "^10.0.0", "@nestjs/common": "^11.0.11",
"@nestjs/core": "^10.0.0", "@nestjs/core": "^11.0.11",
"@nestjs/passport": "^10.0.3", "@nestjs/jwt": "^11.0.0",
"@nestjs/platform-express": "^10.0.0", "@nestjs/mapped-types": "*",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.11",
"axios": "^1.7.2", "axios": "^1.7.2",
"passport-azure-ad": "^4.3.5", "jwks-rsa": "^3.2.0",
"reflect-metadata": "^0.2.0", "passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-openidconnect": "^0.1.2",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1" "rxjs": "^7.8.1"
}, },
"devDependencies": { "devDependencies": {
"@microsoft/microsoft-graph-types": "^2.40.0", "@microsoft/microsoft-graph-types": "^2.40.0",
"@nestjs/cli": "^10.0.0", "@nestjs/cli": "^11.0.5",
"@nestjs/schematics": "^10.0.0", "@nestjs/schematics": "^11.0.2",
"@nestjs/testing": "^10.0.0", "@nestjs/testing": "^11.0.11",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/jest": "^29.5.2", "@types/jest": "^29.5.2",
"@types/node": "^20.3.1", "@types/node": "^20.3.1",
"@types/passport-openidconnect": "^0.1.3",
"@types/supertest": "^6.0.0", "@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0", "@typescript-eslint/parser": "^6.0.0",

View File

@@ -1,4 +1,4 @@
{ {
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"] "exclude": ["node_modules", "**/*.spec.ts"]
} }