99} from "../../common/config.js" ;
1010import {
1111 BaseError ,
12+ DecryptionError ,
1213 EntraFetchError ,
1314 EntraGroupError ,
1415 EntraGroupsFromEmailError ,
@@ -29,18 +30,32 @@ import { UserProfileData } from "common/types/msGraphApi.js";
2930import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager" ;
3031import { DynamoDBClient } from "@aws-sdk/client-dynamodb" ;
3132import { checkPaidMembershipFromTable } from "./membership.js" ;
33+ import { getKey , setKey } from "./redisCache.js" ;
34+ import RedisClient from "ioredis" ;
35+ import type pino from "pino" ;
36+ import { type FastifyBaseLogger } from "fastify" ;
3237
3338export function validateGroupId ( groupId : string ) : boolean {
3439 const groupIdPattern = / ^ [ a - z A - Z 0 - 9 - ] + $ / ; // Adjust the pattern as needed
3540 return groupIdPattern . test ( groupId ) ;
3641}
3742
38- export async function getEntraIdToken (
39- clients : { smClient : SecretsManagerClient ; dynamoClient : DynamoDBClient } ,
40- clientId : string ,
41- scopes : string [ ] = [ "https://graph.microsoft.com/.default" ] ,
42- secretName ?: string ,
43- ) {
43+ type GetEntraIdTokenInput = {
44+ clients : { smClient : SecretsManagerClient ; redisClient : RedisClient . default } ;
45+ encryptionSecret : string ;
46+ clientId : string ;
47+ scopes ?: string [ ] ;
48+ secretName ?: string ;
49+ logger : pino . Logger | FastifyBaseLogger ;
50+ } ;
51+ export async function getEntraIdToken ( {
52+ clients,
53+ encryptionSecret,
54+ clientId,
55+ scopes = [ "https://graph.microsoft.com/.default" ] ,
56+ secretName,
57+ logger,
58+ } : GetEntraIdTokenInput ) {
4459 const localSecretName = secretName || genericConfig . EntraSecretName ;
4560 const secretApiConfig =
4661 ( await getSecretValue ( clients . smClient , localSecretName ) ) || { } ;
@@ -56,12 +71,15 @@ export async function getEntraIdToken(
5671 secretApiConfig . entra_id_private_key as string ,
5772 "base64" ,
5873 ) . toString ( "utf8" ) ;
59- const cachedToken = await getItemFromCache (
60- clients . dynamoClient ,
61- `entra_id_access_token_${ localSecretName } ` ,
62- ) ;
63- if ( cachedToken ) {
64- return cachedToken . token as string ;
74+ const cacheKey = `entra_id_access_token_${ localSecretName } _${ clientId } ` ;
75+ const cachedTokenObject = await getKey < { token : string } > ( {
76+ redisClient : clients . redisClient ,
77+ key : cacheKey ,
78+ encryptionSecret,
79+ logger,
80+ } ) ;
81+ if ( cachedTokenObject ) {
82+ return cachedTokenObject . token ;
6583 }
6684 const config = {
6785 auth : {
@@ -85,13 +103,14 @@ export async function getEntraIdToken(
85103 } ) ;
86104 }
87105 date . setTime ( date . getTime ( ) - 30000 ) ;
88- if ( result ?. accessToken ) {
89- await insertItemIntoCache (
90- clients . dynamoClient ,
91- `entra_id_access_token_${ localSecretName } ` ,
92- { token : result ?. accessToken } ,
93- date ,
94- ) ;
106+ if ( result ?. accessToken && result ?. expiresOn ) {
107+ await setKey ( {
108+ redisClient : clients . redisClient ,
109+ key : cacheKey ,
110+ data : JSON . stringify ( { token : result . accessToken } ) ,
111+ expiresIn : result . expiresOn . getTime ( ) - new Date ( ) . getTime ( ) - 3600 ,
112+ encryptionSecret,
113+ } ) ;
95114 }
96115 return result ?. accessToken ?? null ;
97116 } catch ( error ) {
@@ -508,6 +527,52 @@ export async function isUserInGroup(
508527 }
509528}
510529
530+ /**
531+ * Fetches the ID and display name of groups owned by a specific service principal.
532+ * @param token - An Entra ID token authorized to read service principal information.
533+ */
534+ export async function getServicePrincipalOwnedGroups (
535+ token : string ,
536+ servicePrincipal : string ,
537+ ) : Promise < { id : string ; displayName : string } [ ] > {
538+ try {
539+ // Selects only group objects and retrieves just their id and displayName
540+ const url = `https://graph.microsoft.com/v1.0/servicePrincipals/${ servicePrincipal } /ownedObjects/microsoft.graph.group?$select=id,displayName` ;
541+
542+ const response = await fetch ( url , {
543+ method : "GET" ,
544+ headers : {
545+ Authorization : `Bearer ${ token } ` ,
546+ "Content-Type" : "application/json" ,
547+ } ,
548+ } ) ;
549+
550+ if ( response . ok ) {
551+ const data = ( await response . json ( ) ) as {
552+ value : { id : string ; displayName : string } [ ] ;
553+ } ;
554+ return data . value ;
555+ }
556+
557+ const errorData = ( await response . json ( ) ) as {
558+ error ?: { message ?: string } ;
559+ } ;
560+ throw new EntraFetchError ( {
561+ message : errorData ?. error ?. message ?? response . statusText ,
562+ email : `sp:${ servicePrincipal } ` ,
563+ } ) ;
564+ } catch ( error ) {
565+ if ( error instanceof BaseError ) {
566+ throw error ;
567+ }
568+ const message = error instanceof Error ? error . message : String ( error ) ;
569+ throw new EntraFetchError ( {
570+ message,
571+ email : `sp:${ servicePrincipal } ` ,
572+ } ) ;
573+ }
574+ }
575+
511576export async function listGroupIDsByEmail (
512577 token : string ,
513578 email : string ,
0 commit comments