Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions apps/browser/src/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -5294,5 +5294,17 @@
},
"generatedPassword": {
"message": "Generated password"
},
"useYourCompanyServer": {
"message": "Use your company server"
},
"useUrl": {
"message": "Use my Twake URL"
},
"companyEmail": {
"message": "Company email address"
},
"companyServerError": {
"message": "Company server not found"
}
}
12 changes: 12 additions & 0 deletions apps/browser/src/_locales/fr/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -5293,5 +5293,17 @@
},
"generatedPassword": {
"message": "Mot de passe généré"
},
"useYourCompanyServer": {
"message": "Utiliser le serveur de votre entreprise"
},
"useUrl": {
"message": "Utiliser l'URL de mon Twake"
},
"companyEmail": {
"message": "Adresse e-mail de votre entreprise"
},
"companyServerError": {
"message": "Serveur d'entreprise introuvable"
}
}
58 changes: 48 additions & 10 deletions apps/browser/src/auth/popup/home.component.html
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
<app-header [noTheme]="true"> </app-header>
<div class="center-content">
<div class="content login-page">
<div class="logo-image"></div>
<div class="logo-image" (click)="logoClicked()"></div>
<p class="lead text-center">{{ "loginOrCreateNewAccount" | i18n }}</p>
<form #form [formGroup]="formGroup" (ngSubmit)="submit()">
<form *ngIf="loginMode === 'DEFAULT'" #form [formGroup]="formGroup" (ngSubmit)="submit()">
<div class="box">
<button type="button" class="btn primary block login-button" (click)="openTwakeLogin()">
<b>{{ "login" | i18n }}</b>
</button>
<button type="button" class="btn secondary block login-button" (click)="setCompanyMode()">
<b>{{ "useYourCompanyServer" | i18n }}</b>
</button>
</div>
</form>
<form *ngIf="loginMode === 'URL'" #form [formGroup]="formGroup" (ngSubmit)="submit()">
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<!-- Cozy customization
<label for="email">{{ "emailAddress" | i18n }}</label>
-->
<label for="email">{{ "cozyUrl" | i18n }}</label>
<!-- Cozy customization end -->
<input
id="email"
type="email"
Expand All @@ -20,9 +26,6 @@
placeholder="ex. https://claude.mycozy.cloud"
/>
</div>
<!-- Cozy customization
<environment-selector></environment-selector>
-->
<div class="remember-email-check">
<input
id="rememberEmail"
Expand All @@ -40,7 +43,39 @@
</button>
</div>
</form>
<p class="createAccountLink">
<form *ngIf="loginMode === 'COMPANY'" #form [formGroup]="formGroup" (ngSubmit)="submit()">
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="companyEmail">{{ "companyEmail" | i18n }}</label>
<input
id="companyEmail"
type="companyEmail"
formControlName="companyEmail"
appInputVerbatim="false"
/>
</div>
<div class="remember-email-check">
<input
id="rememberEmail"
type="checkbox"
name="rememberEmail"
formControlName="rememberEmail"
/>
<label for="rememberEmail">{{ "rememberEmail" | i18n }}</label>
</div>
</div>
</div>
<div class="box">
<button type="submit" class="btn primary block login-button" (click)="openCompanyLogin()">
<b>{{ "continue" | i18n }}</b>
</button>
<button type="button" class="btn secondary block login-button" (click)="setUrlMode()">
<b>{{ "useUrl" | i18n }}</b>
</button>
</div>
</form>
<p *ngIf="loginMode === 'DEFAULT'" class="createAccountLink">
{{ "newAroundHere" | i18n }}
<!-- Cozy customization
<a [routerLink]="registerRoute$ | async" (click)="setLoginEmailValues()">{{
Expand All @@ -52,5 +87,8 @@
}}</a>
<!-- Cozy customization end -->
</p>
<p *ngIf="loginMode !== 'DEFAULT'" class="backLink">
<a href="#" (click)="setDefaultMode()">{{ "back" | i18n }}</a>
</p>
</div>
</div>
157 changes: 157 additions & 0 deletions apps/browser/src/auth/popup/home.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,14 @@ import { ToastService } from "@bitwarden/components";
import { BrowserApi } from "../../platform/browser/browser-api";
import { CozySanitizeUrlService } from "../../popup/services/cozySanitizeUrl.service";
import { AccountSwitcherService } from "./account-switching/services/account-switcher.service";
import { getLoginSuccessPageUri, extractDomain } from "../../../src/cozy/sso/helpers";
/* eslint-enable */
/* end Cozy imports */

const DEV_STACK_OAUTHCALLBACK_URI = "https://oauthcallback.cozy.wtf";
const INT_STACK_OAUTHCALLBACK_URI = "https://oauthcallback.cozy.works";
const PROD_STACK_OAUTHCALLBACK_URI = "https://oauthcallback.mycozy.cloud";

@Component({
selector: "app-home",
templateUrl: "home.component.html",
Expand All @@ -27,15 +32,22 @@ export class HomeComponent implements OnInit, OnDestroy {
private destroyed$: Subject<void> = new Subject();

loginInitiated = false;
loginMode = "DEFAULT";
formGroup = this.formBuilder.group({
/** Cozy custo
email: ["", [Validators.required, Validators.email]],
*/
email: [""],
companyEmail: [""],
/** end custo */
rememberEmail: [false],
});

// Cozy customization; to change stack URI
logoClickCount = 0;
baseUri = PROD_STACK_OAUTHCALLBACK_URI;
// Cozy customization

// TODO: remove when email verification flag is removed
registerRoute$ = this.registerRouteService.registerRoute$();

Expand Down Expand Up @@ -65,6 +77,8 @@ export class HomeComponent implements OnInit, OnDestroy {
}
}

this.redirectIfSSOLoginSuccessTab();

// Cozy customization
/*
this.environmentSelector.onOpenSelfHostedSettings
Expand Down Expand Up @@ -148,6 +162,29 @@ export class HomeComponent implements OnInit, OnDestroy {
}
/* end custo */

/* Cozy custo */
openTwakeLogin() {
const extensionUri = this.platformUtilsService.getExtensionUri();
const redirectUri = getLoginSuccessPageUri(extensionUri);

BrowserApi.createNewTab(`${this.baseUri}/oidc/bitwarden/twake?redirect_uri=${redirectUri}`);
}
/* end custo */

/* Cozy custo */
setDefaultMode() {
this.loginMode = "DEFAULT";
}

setCompanyMode() {
this.loginMode = "COMPANY";
}

setUrlMode() {
this.loginMode = "URL";
}
/* end custo */

// Cozy customization; check if Cozy exists before navigating to login page
async cozyExist(cozyUrl: string) {
const preloginCozyUrl = new URL("/public/prelogin", cozyUrl).toString();
Expand All @@ -163,4 +200,124 @@ export class HomeComponent implements OnInit, OnDestroy {
}
}
// Cozy customization end

async getLoginUri(companyEmail: string): Promise<URL | null> {
try {
const domain = extractDomain(companyEmail);

if (!domain) {
throw new Error();
}

const extensionUri = this.platformUtilsService.getExtensionUri();
const redirectUri = getLoginSuccessPageUri(extensionUri);

const uriFromWellKnown = await this.fetchLoginUriWithWellKnown(domain);

if (uriFromWellKnown) {
uriFromWellKnown.searchParams.append("redirect_uri", redirectUri);
return uriFromWellKnown;
}

return null;
} catch {
return null;
}
}

private async fetchLoginUriWithWellKnown(domain: string): Promise<URL | null> {
const url = `https://${domain}/.well-known/twake-configuration`;

try {
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
});

if (response.ok) {
const twakeConfiguration = await response.json();

return new URL(twakeConfiguration["twake-pass-login-uri"]) || null;
} else {
return null;
}
} catch {
return null;
}
}

async openCompanyLogin() {
const companyEmail = this.formGroup.value.companyEmail;

if (!companyEmail) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccured"),
message: this.i18nService.t("emailRequired"),
});
return;
}

const loginUri = await this.getLoginUri(companyEmail);

if (loginUri) {
BrowserApi.createNewTab(loginUri.toString());
} else {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccured"),
message: this.i18nService.t("companyServerError"),
});
}
}

// Cozy customization
async redirectIfSSOLoginSuccessTab() {
chrome.tabs.query({}, (tabs) => {
const extensionUri = this.platformUtilsService.getExtensionUri();
const redirectUri = getLoginSuccessPageUri(extensionUri);

const SSOLoginSuccessTab = tabs.find(
(tab) => tab.status === "complete" && tab.url.startsWith(redirectUri),
);

if (SSOLoginSuccessTab) {
const url = new URL(SSOLoginSuccessTab.url);
const instance = url.searchParams.get("instance");
const code = url.searchParams.get("code");

this.router.navigate(["login"], {
queryParams: { email: instance, cozyUrl: instance, code },
});
}
});
}
// Cozy customization end

// Cozy customization
async logoClicked() {
this.logoClickCount++;

if (this.logoClickCount >= 6) {
const rest = this.logoClickCount % 3;

if (rest === 0) {
this.baseUri = DEV_STACK_OAUTHCALLBACK_URI;
} else if (rest === 1) {
this.baseUri = INT_STACK_OAUTHCALLBACK_URI;
} else if (rest === 2) {
this.baseUri = PROD_STACK_OAUTHCALLBACK_URI;
}

Comment on lines +302 to +314
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm not sure about this part, we change the environment based on the number of clicks we do on the application logo?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly

this.toastService.showToast({
variant: "info",
title: "New base URI",
message: this.baseUri,
});
}
}
// Cozy customization end
}
2 changes: 2 additions & 0 deletions apps/browser/src/auth/popup/login-v1.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit {
// This adds the scheme if missing
await this.environmentService.setEnvironment(Region.SelfHosted, {
base: this.cozyUrl + "/bitwarden",
oidc: this.cozyUrl + "/oidc/bitwarden",
});

// The email is based on the URL and necessary for login
Expand All @@ -240,6 +241,7 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit {
data.masterPassword,
null,
null,
this.code,
);

this.formPromise = this.loginStrategyService.logIn(credentials);
Expand Down
Loading