diff --git a/apps/web/src/app/tools/send/send-access/authentication-flow.md b/apps/web/src/app/tools/send/send-access/authentication-flow.md index 7fa253af3bb..f39b43fcd41 100644 --- a/apps/web/src/app/tools/send/send-access/authentication-flow.md +++ b/apps/web/src/app/tools/send/send-access/authentication-flow.md @@ -9,7 +9,7 @@ Anyone can access a public send. The token endpoint automatically issues a token ```mermaid sequenceDiagram participant Visitor - participant TryAccess as try-access.component + participant TryAccess as try-send-access.guard participant SendToken as send-token API participant ViewContent as view-content.component participant SendAccess as send-access API @@ -34,7 +34,7 @@ Password protected sends redirect to a password challenge prompt. ```mermaid sequenceDiagram participant Visitor - participant TryAccess as try-access.component + participant TryAccess as try-send-access.guard participant PasswordAuth as password-authentication.component participant SendToken as send-token API participant ViewContent as view-content.component @@ -68,7 +68,7 @@ Visiting the view page without a token redirects to a try-access flow, above. sequenceDiagram participant Visitor participant ViewContent as view-content.component - participant TryAccess as try-access.component + participant TryAccess as try-send-access.guard Visitor->>ViewContent: Navigate to send URL (with id and key) ViewContent->>TryAccess: Redirect to try-access (with id and key) diff --git a/apps/web/src/app/tools/send/send-access/authentication.prompts.md b/apps/web/src/app/tools/send/send-access/authentication.prompts.md new file mode 100644 index 00000000000..aadd837d862 --- /dev/null +++ b/apps/web/src/app/tools/send/send-access/authentication.prompts.md @@ -0,0 +1,34 @@ +# Documentation prompts + +The following prompts were used with Claude 4 - Sonnet to write send documentation. + +## Authentication Flow Diagrams + +Turn a list of steps into a sequence diagram. + +``` +Create a mermaid sequence diagram for the Bitwarden Send protocol authentication flow. Use these components: + +**Components:** +- `try-access.component` - Entry point that attempts to access a send +- `password-authentication.component` - Handles password-based authentication +- `view-content.component` - Displays the send content +- `send-token API` - Server endpoint that issues security tokens + +**Flow Steps:** +1. [Visitor navigates to try-access.] +2. [try-access requests anonymous access from the send-token API] +3. [send-token service replies with 200 and a token.] +4. [try-access redirects to view-content with the token.] +5. [view-content requests the send content from the send-access API with the token.] +6. [the send-access API returns the send content] +[ADD MORE STEPS AS NEEDED] + +**Requirements:** +- Use clear, descriptive labels for each interaction +- Include HTTP status codes where relevant +- Show conditional logic with alt/opt blocks for decision points +- Use proper mermaid sequence diagram syntax +- Generate ONLY the mermaid code block, no additional explanation +- Follow the exact component names provided above +``` diff --git a/apps/web/src/app/tools/send/send-access/send-access-memory.ts b/apps/web/src/app/tools/send/send-access/send-access-memory.ts new file mode 100644 index 00000000000..db0d70b0273 --- /dev/null +++ b/apps/web/src/app/tools/send/send-access/send-access-memory.ts @@ -0,0 +1,32 @@ +import { KeyDefinition, SEND_ACCESS_AUTH_MEMORY } from "@bitwarden/common/platform/state"; +import { SendAccessResponse } from "@bitwarden/common/tools/send/models/response/send-access.response"; + +export type SendContext = { + id: string; + key: string; +}; + +export function isSendContext(value: unknown): value is SendContext { + return !!value && typeof value === "object" && "id" in value && "key" in value; +} + +export const SEND_CONTEXT_KEY = new KeyDefinition( + SEND_ACCESS_AUTH_MEMORY, + "sendContext", + { + deserializer: (data) => data, + }, +); + +/** When send authentication succeeds, this stores the result so that + * multiple access attempts don't accrue due to the send workflow. + */ +// FIXME: replace this with the send authentication token once it's +// available +export const SEND_RESPONSE_KEY = new KeyDefinition( + SEND_ACCESS_AUTH_MEMORY, + "sendResponse", + { + deserializer: (data) => (data ? new SendAccessResponse(data) : null), + }, +); diff --git a/apps/web/src/app/tools/send/send-access/send-access-authentication.service.ts b/apps/web/src/app/tools/send/send-access/send-access.service.ts similarity index 84% rename from apps/web/src/app/tools/send/send-access/send-access-authentication.service.ts rename to apps/web/src/app/tools/send/send-access/send-access.service.ts index fdb228ea6bf..a867aaa8f9b 100644 --- a/apps/web/src/app/tools/send/send-access/send-access-authentication.service.ts +++ b/apps/web/src/app/tools/send/send-access/send-access.service.ts @@ -9,13 +9,13 @@ import { SystemServiceProvider } from "@bitwarden/common/tools/providers.js"; import { SendAccessRequest } from "@bitwarden/common/tools/send/models/request/send-access.request"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; -import { TOKEN_KEY, SEND_KEY_KEY } from "./send-access-memory"; +import { SEND_RESPONSE_KEY, SEND_CONTEXT_KEY } from "./send-access-memory"; import { isErrorResponse } from "./util"; const TEN_SECONDS = 10_000; @Injectable({ providedIn: "root" }) -export class SendAccessAuthenticationService { +export class SendAccessService { private readonly logger: SemanticLogger; constructor( @@ -31,6 +31,8 @@ export class SendAccessAuthenticationService { } redirect$(sendId: string) { + // FIXME: when the send authentication APIs become available, this method + // should delegate to the API const response$ = from(this.api.postSendAccess(sendId, new SendAccessRequest())); const redirect$ = response$.pipe( @@ -84,16 +86,12 @@ export class SendAccessAuthenticationService { return url; } - async setToken(token: string) { - return this.state.getGlobal(TOKEN_KEY).update(() => token); - } - - async setKey(key: string) { - return this.state.getGlobal(SEND_KEY_KEY).update(() => key); + async setContext(sendId: string, key: string) { + return this.state.getGlobal(SEND_CONTEXT_KEY).update(() => ({ id: sendId, key })); } async clear(): Promise { - await this.state.getGlobal(TOKEN_KEY).update(() => null); - await this.state.getGlobal(SEND_KEY_KEY).update(() => null); + await this.state.getGlobal(SEND_RESPONSE_KEY).update(() => null); + await this.state.getGlobal(SEND_CONTEXT_KEY).update(() => null); } } diff --git a/apps/web/src/app/tools/send/send-access/try-send-access.guard.spec.ts b/apps/web/src/app/tools/send/send-access/try-send-access.guard.spec.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/web/src/app/tools/send/send-access/try-send-access.guard.ts b/apps/web/src/app/tools/send/send-access/try-send-access.guard.ts index 46a92e5e4d3..1c6e9344f94 100644 --- a/apps/web/src/app/tools/send/send-access/try-send-access.guard.ts +++ b/apps/web/src/app/tools/send/send-access/try-send-access.guard.ts @@ -2,18 +2,37 @@ import { inject } from "@angular/core"; import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } from "@angular/router"; import { from, ignoreElements, concat } from "rxjs"; -import { SendAccessAuthenticationService } from "./send-access-authentication.service"; +import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; +import { SYSTEM_SERVICE_PROVIDER } from "@bitwarden/generator-components"; + +import { SendAccessService } from "./send-access.service"; export const trySendAccess: CanActivateFn = ( route: ActivatedRouteSnapshot, - state: RouterStateSnapshot, + _state: RouterStateSnapshot, ) => { - const sendAccess = inject(SendAccessAuthenticationService); + const sendAccess = inject(SendAccessService); + const system = inject(SYSTEM_SERVICE_PROVIDER); + const logger = system.log({ function: "trySendAccess" }); const { sendId, key } = route.params; + if (!sendId) { + logger.warn("sendId missing from the route parameters; redirecting to 404"); + } + if (typeof sendId !== "string") { + logger.panic({ expected: "string", actual: typeof sendId }, "sendId has invalid type"); + } - const setKey$ = from(sendAccess.setKey(key)).pipe(ignoreElements()); + if (!key) { + logger.panic("key missing from the route parameters"); + } + if (typeof key !== "string") { + logger.panic({ expected: "string", actual: typeof key }, "key has invalid type"); + } + + const contextUpdated$ = from(sendAccess.setContext(sendId, key)).pipe(ignoreElements()); const redirect$ = sendAccess.redirect$(sendId); - return concat(setKey$, redirect$); + // ensure the key has loaded before redirecting + return concat(contextUpdated$, redirect$); }; diff --git a/libs/tools/generator/components/src/generator-services.module.ts b/libs/tools/generator/components/src/generator-services.module.ts index 3a7b771a25d..946f7196d85 100644 --- a/libs/tools/generator/components/src/generator-services.module.ts +++ b/libs/tools/generator/components/src/generator-services.module.ts @@ -40,7 +40,12 @@ export const RANDOMIZER = new SafeInjectionToken("Randomizer"); const GENERATOR_SERVICE_PROVIDER = new SafeInjectionToken( "CredentialGeneratorProviders", ); -const SYSTEM_SERVICE_PROVIDER = new SafeInjectionToken("SystemServices"); + +// FIXME: relocate the system service provider to a more general module once +// NX migration is complete. +export const SYSTEM_SERVICE_PROVIDER = new SafeInjectionToken( + "SystemServices", +); /** Shared module containing generator component dependencies */ @NgModule({ diff --git a/libs/tools/generator/components/src/index.ts b/libs/tools/generator/components/src/index.ts index 56eb912f367..4ec32032de0 100644 --- a/libs/tools/generator/components/src/index.ts +++ b/libs/tools/generator/components/src/index.ts @@ -2,4 +2,4 @@ export { CredentialGeneratorHistoryComponent } from "./credential-generator-hist export { CredentialGeneratorHistoryDialogComponent } from "./credential-generator-history-dialog.component"; export { EmptyCredentialHistoryComponent } from "./empty-credential-history.component"; export { GeneratorModule } from "./generator.module"; -export { GeneratorServicesModule } from "./generator-services.module"; +export { GeneratorServicesModule, SYSTEM_SERVICE_PROVIDER } from "./generator-services.module";