Skip to content

Commit

Permalink
Address re-init token issues
Browse files Browse the repository at this point in the history
  • Loading branch information
sleepyfran committed Aug 27, 2024
1 parent 01316f0 commit dfd1377
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 33 deletions.
27 changes: 27 additions & 0 deletions packages/core/types/src/model/authentication.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
import * as S from "@effect/schema/Schema";

/**
* Defines all the provider-specific information that is needed to authenticate
* with a specific provider.
*/
export const ProviderSpecificAuthenticationInfo = S.Union(
S.TaggedStruct("MSAL", {
account: S.Struct({
homeAccountId: S.String.pipe(S.nonEmptyString()),
environment: S.String.pipe(S.nonEmptyString()),
tenantId: S.String.pipe(S.nonEmptyString()),
username: S.String.pipe(S.nonEmptyString()),
localAccountId: S.String.pipe(S.nonEmptyString()),
}),
}),
);
export type ProviderSpecificAuthenticationInfo = S.Schema.Type<
typeof ProviderSpecificAuthenticationInfo
>;

/**
* Defines the result of a successfully authenticated user, with the information
* that is needed to use a service that requires authentication.
Expand All @@ -10,6 +29,12 @@ export const AuthenticationInfoSchema = S.Struct({
*/
accessToken: S.String.pipe(S.nonEmptyString()),

/**
* Provider-specific information that is needed to authenticate with the
* provider.
*/
providerSpecific: ProviderSpecificAuthenticationInfo,

/**
* Date in which the token expires.
*/
Expand All @@ -21,5 +46,7 @@ export type AuthenticationInfo = S.Schema.Type<typeof AuthenticationInfoSchema>;
* Defines the error that can occur when authenticating a user.
*/
export enum AuthenticationError {
InteractionRequired = "InteractionRequired",
Unknown = "Unknown",
WrongCredentials = "WrongCredentials",
}
9 changes: 9 additions & 0 deletions packages/core/types/src/services/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,13 @@ export type Authentication = {
* Implements the authentication flow for a specific provider.
*/
connect: Effect<AuthenticationInfo, AuthenticationError>;

/**
* Attempts to silently authenticate the user with the cached credentials,
* if available. If the credentials are not available, the service will run
* the connect flow.
*/
connectSilent: (
cachedCredentials: AuthenticationInfo,
) => Effect<AuthenticationInfo, AuthenticationError>;
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { PublicClientApplication } from "@azure/msal-browser";
import {
InteractionRequiredAuthError,
PublicClientApplication,
type AuthenticationResult as MsalAuthenticationResult,
} from "@azure/msal-browser";
import { addHours } from "@echo/core-dates";
import {
AppConfig,
type Authentication,
AuthenticationError,
type AuthenticationInfo,
ProviderSpecificAuthenticationInfo,
} from "@echo/core-types";
import { Context, Effect, Layer, Ref } from "effect";

Expand Down Expand Up @@ -33,29 +39,78 @@ export const MsalAuthenticationLive = Layer.effect(
}),
);

return MsalAuthentication.of({
connect: Effect.gen(function* () {
const app = yield* msalAppRef.get;

yield* Effect.tryPromise({
try: () => app.initialize(),
catch: () => AuthenticationError.Unknown,
});

const authResult = yield* Effect.tryPromise({
try: () => app.loginPopup({ scopes: [...appConfig.graph.scopes] }),
catch: () => AuthenticationError.Unknown,
});
const authRequest = { scopes: [...appConfig.graph.scopes] };

const handleResponse = (
authResult: MsalAuthenticationResult | null,
): Effect.Effect<AuthenticationInfo, AuthenticationError> =>
Effect.gen(function* () {
if (authResult) {
return {
accessToken: authResult.accessToken,
expiresOn: authResult.expiresOn ?? addHours(new Date(), 2),
providerSpecific: ProviderSpecificAuthenticationInfo.make({
account: authResult.account,
}),
};
}

return yield* Effect.fail(AuthenticationError.Unknown);
}),
});

const connect = Effect.gen(function* () {
const app = yield* msalAppRef.get;

yield* Effect.tryPromise({
try: () => app.initialize(),
catch: () => AuthenticationError.Unknown,
});

const authResult = yield* Effect.tryPromise({
try: () => app.loginPopup(authRequest),
catch: () => AuthenticationError.Unknown,
});

return yield* handleResponse(authResult);
});

const connectSilent = (cachedCredentials: AuthenticationInfo) =>
Effect.gen(function* () {
const app = yield* msalAppRef.get;

if (cachedCredentials.providerSpecific._tag !== "MSAL") {
return yield* Effect.fail(AuthenticationError.WrongCredentials);
}

yield* Effect.tryPromise({
try: () => app.initialize(),
catch: () => AuthenticationError.Unknown,
});

return yield* Effect.tryPromise({
try: () =>
app.acquireTokenSilent({
...authRequest,
account: cachedCredentials.providerSpecific.account,
}),
catch: (e) => {
console.error(e);
return e instanceof InteractionRequiredAuthError
? AuthenticationError.InteractionRequired
: AuthenticationError.Unknown;
},
}).pipe(
Effect.tapError((e) =>
Effect.logError(`Error while connecting silently: ${e}`),
),
Effect.flatMap(handleResponse),
Effect.orElse(() => connect),
);
});

return MsalAuthentication.of({
connect,
connectSilent,
});
}),
);
31 changes: 18 additions & 13 deletions packages/services/app-init/src/app-init.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { isValidToken } from "@echo/core-auth";
import {
AppInit,
AvailableProviders,
LocalStorage,
MediaPlayerFactory,
MediaProviderFactory,
MediaProviderMainThreadBroadcastChannel,
type ProviderStartArgs,
type BroadcastChannel,
Expand All @@ -18,6 +16,7 @@ import {
LazyLoadedMediaPlayer,
LazyLoadedProvider,
} from "@echo/services-bootstrap";
import type { ILoadedProvider } from "@echo/services-bootstrap/src/loaders/provider";
import { Effect, Layer, Option } from "effect";

const make = Effect.gen(function* () {
Expand Down Expand Up @@ -46,11 +45,11 @@ const make = Effect.gen(function* () {

return yield* reinitializeProvider(
providerStartArgs.value,
providerFactory.createMediaProvider,
providerFactory,
mediaPlayerFactory.createMediaPlayer,
broadcastChannel,
activeMediaProviderCache,
);
).pipe(Effect.orElseSucceed(() => {}));
}),
),
);
Expand All @@ -76,31 +75,37 @@ const retrieveProviderArgs = (

const reinitializeProvider = (
startArgs: ProviderStartArgs,
createMediaProvider: MediaProviderFactory["createMediaProvider"],
providerFactory: ILoadedProvider,
createMediaPlayer: MediaPlayerFactory["createMediaPlayer"],
broadcastChannel: BroadcastChannel<
MediaProviderBroadcastSchema["mainThread"]
>,
activeMediaProviderCache: IActiveMediaProviderCache,
) =>
Effect.gen(function* () {
// TODO: This should attempt to refresh the token if it's expired.
// TODO: Instead of ignoring the initialization with a warning, we should notify the user.
if (!isValidToken(startArgs.authInfo)) {
yield* Effect.logWarning(
`The retrieved token for ${startArgs.metadata.id} is invalid, ignoring initialization.`,
const authResult = yield* providerFactory.authentication
.connectSilent(startArgs.authInfo)
.pipe(
Effect.tapError((error) =>
Effect.logWarning(
`Failed to silently authenticate with ${startArgs.metadata.id}, ignoring cached credentials. Error: ${error}`,
),
),
);
}

const mediaProvider = createMediaProvider(startArgs.authInfo);
const mediaPlayer = yield* createMediaPlayer(startArgs.authInfo);
const mediaProvider = providerFactory.createMediaProvider(authResult);
const mediaPlayer = yield* createMediaPlayer(authResult);

yield* broadcastChannel.send("start", startArgs);
yield* activeMediaProviderCache.add(
startArgs.metadata,
mediaProvider,
mediaPlayer,
);

yield* Effect.log(
`Successfully reinitialized ${startArgs.metadata.id} provider`,
);
});

export const AppInitLive = Layer.effect(AppInit, make);
15 changes: 10 additions & 5 deletions packages/services/bootstrap/src/loaders/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,20 @@ import {
} from "@echo/core-types";
import { AppConfigLive } from "../app-config";

/**
* Represents the available data for a loaded provider.
*/
export type ILoadedProvider = {
metadata: ProviderMetadata;
authentication: Authentication;
createMediaProvider: MediaProviderFactory["createMediaProvider"];
};

/**
* Service that can lazily load a media provider.
*/
export type ILazyLoadedProvider = {
readonly load: (metadata: ProviderMetadata) => Effect.Effect<{
metadata: ProviderMetadata;
authentication: Authentication;
createMediaProvider: MediaProviderFactory["createMediaProvider"];
}>;
readonly load: (metadata: ProviderMetadata) => Effect.Effect<ILoadedProvider>;
};

/**
Expand Down

0 comments on commit dfd1377

Please sign in to comment.