• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

alkem-io / server / #8514

11 Oct 2024 02:20PM UTC coverage: 13.607%. First build
#8514

Pull #4609

travis-ci

Pull Request #4609: [v0.93.0] Roles API, Unauthenticated Explore page (Merge conflict fix)

79 of 4356 branches covered (1.81%)

Branch coverage included in aggregate %.

83 of 636 new or added lines in 25 files covered. (13.05%)

1946 of 10526 relevant lines covered (18.49%)

3.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

21.05
/src/core/authentication/authentication.service.ts
1
import { ConfigService } from '@nestjs/config';
1✔
2
import { Inject, Injectable, LoggerService } from '@nestjs/common';
1✔
3
import { Session } from '@ory/kratos-client';
1✔
4
import { LogContext } from '@common/enums';
5
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
6
import { AgentInfo } from '../authentication.agent.info/agent.info';
7
import { NotSupportedException } from '@common/exceptions';
8
import { AgentInfoCacheService } from '@core/authentication.agent.info/agent.info.cache.service';
9
import ConfigUtils from '@config/config.utils';
1✔
10
import { AlkemioConfig } from '@src/types';
1✔
11
import { KratosService } from '@services/infrastructure/kratos/kratos.service';
1✔
12
import { OryDefaultIdentitySchema } from '@services/infrastructure/kratos/types/ory.default.identity.schema';
1✔
13
import { OryTraits } from '@services/infrastructure/kratos/types/ory.traits';
14
import { AgentInfoService } from '@core/authentication.agent.info/agent.info.service';
15
import { AgentService } from '@domain/agent/agent/agent.service';
16

17
@Injectable()
1✔
18
export class AuthenticationService {
1✔
19
  private readonly extendSessionMinRemainingTTL: number | undefined; // min time before session expires when it's already allowed to be extended (in milliseconds)
1✔
20

1✔
21
  constructor(
22
    private agentInfoCacheService: AgentInfoCacheService,
1✔
23
    private agentInfoService: AgentInfoService,
1✔
24
    private configService: ConfigService<AlkemioConfig, true>,
1✔
25
    private kratosService: KratosService,
26
    private agentService: AgentService,
27
    @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService
28
  ) {
29
    const { earliest_possible_extend } = this.configService.get(
1✔
30
      'identity.authentication.providers.ory',
31
      {
32
        infer: true,
33
      }
34
    );
35

36
    this.extendSessionMinRemainingTTL = this.parseEarliestPossibleExtend(
37
      earliest_possible_extend
38
    );
1✔
39
  }
1✔
40

1✔
41
  public async getAgentInfo(opts: {
1✔
42
    cookie?: string;
1✔
43
    authorization?: string;
44
  }): Promise<AgentInfo> {
1✔
45
    let session: Session | undefined;
46
    try {
47
      session = await this.kratosService.getSession(
48
        opts.authorization,
49
        opts.cookie
50
      );
51
    } catch {
52
      return this.agentInfoService.createAnonymousAgentInfo();
53
    }
1✔
54

55
    if (!session?.identity) {
56
      return this.agentInfoService.createAnonymousAgentInfo();
1✔
57
    }
1✔
58

59
    const oryIdentity = session.identity as OryDefaultIdentitySchema;
1✔
60
    return this.createAgentInfo(oryIdentity);
1✔
61
  }
62

1✔
63
  /**
64
   * Adds verified credentials to the agent information if SSI (Self-Sovereign Identity) is enabled.
65
   *
66
   * @param agentInfo - The information of the agent to which verified credentials will be added.
1✔
67
   * @param agentID - The unique identifier of the agent.
68
   * @returns A promise that resolves when the operation is complete.
69
   */
70
  public async addVerifiedCredentialsIfEnabled(
71
    agentInfo: AgentInfo,
72
    agentID: string
1✔
73
  ): Promise<void> {
74
    const ssiEnabled = this.configService.get('ssi.enabled', { infer: true });
75
    if (ssiEnabled) {
76
      const verifiedCredentials =
77
        await this.agentService.getVerifiedCredentials(agentID);
78
      agentInfo.verifiedCredentials = verifiedCredentials;
79
    }
80
  }
81

×
82
  /**
×
83
   * Creates and returns an `AgentInfo` object based on the provided Ory identity and session.
84
   *
×
85
   * @param oryIdentity - Optional Ory identity schema containing user traits.
86
   * @param session - Optional session information.
87
   * @returns A promise that resolves to an `AgentInfo` object.
×
88
   *
×
89
   * This method performs the following steps:
90
   * 1. Validates the provided Ory identity.
91
   * 2. Checks for cached agent information based on the email from the Ory identity.
×
92
   * 3. Builds basic agent information if no cached information is found.
×
93
   * 4. Maps the authentication type from the session.
94
   * 5. Retrieves additional metadata for the agent.
95
   * 6. Populates the agent information with the retrieved metadata.
96
   * 7. Adds verified credentials if enabled.
97
   * 8. Caches the agent information.
98
   */
99
  async createAgentInfo(
100
    oryIdentity?: OryDefaultIdentitySchema,
101
    session?: Session
102
  ): Promise<AgentInfo> {
103
    if (!oryIdentity) return this.agentInfoService.createAnonymousAgentInfo();
104

105
    const oryTraits = this.validateEmail(oryIdentity);
106

107
    const cachedAgentInfo = await this.getCachedAgentInfo(oryTraits.email);
108
    if (cachedAgentInfo) return cachedAgentInfo;
109

110
    const agentInfo = this.buildAgentInfoFromOrySession(oryIdentity, session);
111

112
    const agentInfoMetadata = await this.agentInfoService.getAgentInfoMetadata(
113
      agentInfo.email
114
    );
115
    if (!agentInfoMetadata) return agentInfo;
NEW
116

×
117
    this.agentInfoService.populateAgentInfoWithMetadata(
NEW
118
      agentInfo,
×
119
      agentInfoMetadata
NEW
120
    );
×
NEW
121
    await this.addVerifiedCredentialsIfEnabled(
×
122
      agentInfo,
NEW
123
      agentInfoMetadata.agentID
×
124
    );
NEW
125

×
NEW
126
    await this.agentInfoCacheService.setAgentInfoCache(agentInfo);
×
127
    return agentInfo;
NEW
128
  }
×
NEW
129

×
130
  /**
131
   * Validates the email trait of the provided Ory identity.
132
   *
133
   * @param oryIdentity - The Ory identity schema containing traits to be validated.
NEW
134
   * @returns The validated Ory traits.
×
NEW
135
   * @throws NotSupportedException - If the email trait is missing or empty.
×
136
   */
137
  private validateEmail(oryIdentity: OryDefaultIdentitySchema): OryTraits {
138
    const oryTraits = oryIdentity.traits;
139
    if (!oryTraits.email || oryTraits.email.length === 0) {
140
      throw new NotSupportedException(
141
        'Session without email encountered',
142
        LogContext.AUTH
143
      );
144
    }
145
    return oryTraits;
146
  }
×
147

×
148
  /**
×
149
   * Retrieves the cached agent information for a given email.
150
   *
151
   * @param email - The email address of the agent.
152
   * @returns A promise that resolves to the agent information if found in the cache, or undefined if not found.
NEW
153
   */
×
154
  private async getCachedAgentInfo(
155
    email: string
156
  ): Promise<AgentInfo | undefined> {
157
    return await this.agentInfoCacheService.getAgentInfoFromCache(email);
158
  }
159

160
  /**
161
   * Builds and returns an `AgentInfo` object based on the provided Ory identity schema and session.
162
   *
163
   * @param oryIdentity - The Ory identity schema containing user traits and verifiable addresses.
164
   * @param session - Optional session object containing session details like expiration time.
NEW
165
   * @returns An `AgentInfo` object populated with the user's email, name, avatar URL, and session expiry.
×
166
   */
167
  private buildAgentInfoFromOrySession(
168
    oryIdentity: OryDefaultIdentitySchema,
169
    session?: Session
170
  ): AgentInfo {
171
    const agentInfo = new AgentInfo();
172
    const oryTraits = oryIdentity.traits;
173
    const isEmailVerified =
174
      oryIdentity.verifiable_addresses.find(x => x.via === 'email')?.verified ??
175
      false;
176

177
    agentInfo.email = oryTraits.email;
178
    agentInfo.emailVerified = isEmailVerified;
NEW
179
    agentInfo.firstName = oryTraits.name.first;
×
NEW
180
    agentInfo.lastName = oryTraits.name.last;
×
181
    agentInfo.avatarURL = oryTraits.picture;
182
    agentInfo.expiry = session?.expires_at
×
183
      ? new Date(session.expires_at).getTime()
184
      : undefined;
185

×
186
    return agentInfo;
×
187
  }
×
188

×
189
  public async extendSession(sessionToBeExtended: Session): Promise<void> {
×
190
    const adminBearerToken = await this.kratosService.getBearerToken();
×
191

192
    return this.kratosService.tryExtendSession(
193
      sessionToBeExtended,
NEW
194
      adminBearerToken
×
195
    );
196
  }
197

198
  /**
199
   * Determines whether a session should be extended based on its expiration time and a minimum remaining TTL (Time To Live).
200
   *
201
   * @param session - The session object containing the expiration time.
202
   * @returns `true` if the session should be extended, `false` otherwise.
203
   *
204
   * The function checks the following conditions:
205
   * - If the session does not have an expiration time (`expires_at`) or the minimum remaining TTL (`extendSessionMinRemainingTTL`) is not set, it returns `false`.
206
   * - If the minimum remaining TTL is set to `-1`, it returns `true`, indicating that the session can be extended at any time.
207
   * - Otherwise, it calculates the session's expiry time and compares it with the current time plus the minimum remaining TTL to determine if the session should be extended.
×
NEW
208
   */
×
209
  public shouldExtendSession(session: Session): boolean {
210
    if (!session.expires_at || !this.extendSessionMinRemainingTTL) {
×
211
      return false;
212
    }
213
    if (this.extendSessionMinRemainingTTL === -1) {
NEW
214
      return true; // Set to -1 if specified as lifespan in config, meaning it can be extended at any time
×
215
    }
216

217
    const expiry = new Date(session.expires_at);
218
    return Date.now() >= expiry.getTime() - this.extendSessionMinRemainingTTL;
219
  }
220

221
  /**
222
   * Parses the `earliestPossibleExtend` parameter to determine the earliest possible time to extend a session.
223
   *
224
   * If the `earliestPossibleExtend` is set to 'lifespan', it returns -1, allowing sessions to be refreshed during their entire lifespan.
225
   * If the `earliestPossibleExtend` is a string, it attempts to parse it as a time duration in HMS format and returns the equivalent milliseconds.
226
   * If the parsing fails or the input is of an unexpected type, it returns `undefined`.
227
   *
228
   * @param earliestPossibleExtend - The input value representing the earliest possible time to extend a session. It can be 'lifespan' or a string in HMS format.
229
   * @returns The earliest possible extend time in milliseconds, -1 for 'lifespan', or `undefined` if the input is invalid.
230
   */
231
  private parseEarliestPossibleExtend(
232
    earliestPossibleExtend: unknown
NEW
233
  ): number | undefined {
×
NEW
234
    /**
×
NEW
235
     * If you need high flexibility when extending sessions, you can set earliest_possible_extend to lifespan,
×
236
     * which allows sessions to be refreshed during their entire lifespan, even right after they are created.
NEW
237
     * Source https://www.ory.sh/docs/kratos/session-management/refresh-extend-sessions
×
NEW
238
     */
×
239
    if (earliestPossibleExtend === 'lifespan') {
240
      return -1;
×
241
    }
242
    if (typeof earliestPossibleExtend === 'string') {
243
      const seconds = ConfigUtils.parseHMSString(earliestPossibleExtend);
244
      if (seconds) {
245
        return seconds * 1000;
246
      }
247
    }
248
    return undefined;
249
  }
250
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc