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
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ services:
- SM_IP=localhost
# - GOOGLE_RECAPTCHA=
# - ALLOW_INTERNAL_AUTH=false
# - ALLOW_SAML_AUTH=false
# - ALLOW_HOME_VIEW=true
# - GITHUB_CLIENTID=
# - BITBUCKET_CLIENTID=
Expand Down
1 change: 1 addition & 0 deletions docker/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ echo "SWITCHERAPI_URL = ${SWITCHERAPI_URL}"
echo "SM_IP = ${SM_IP}"
echo "GOOGLE_RECAPTCHA = ${GOOGLE_RECAPTCHA}"
echo "ALLOW_INTERNAL_AUTH = ${ALLOW_INTERNAL_AUTH}"
echo "ALLOW_SAML_AUTH = ${ALLOW_SAML_AUTH}"
echo "ALLOW_HOME_VIEW = ${ALLOW_HOME_VIEW}"
echo "GITHUB_CLIENTID = ${GITHUB_CLIENTID}"
echo "BITBUCKET_CLIENTID = ${BITBUCKET_CLIENTID}"
Expand Down
411 changes: 228 additions & 183 deletions npm-shrinkwrap.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@
"@angular/cli": "^20.3.1",
"@angular/compiler-cli": "~20.3.0",
"@angular/language-service": "20.3.0",
"@types/node": "^24.3.1",
"@types/react": "^19.1.12",
"angular-eslint": "20.2.0",
"@types/node": "^24.4.0",
"@types/react": "^19.1.13",
"angular-eslint": "20.3.0",
"eslint": "^9.35.0",
"ts-node": "^10.9.2",
"typescript": "^5.9.2",
Expand Down
29 changes: 22 additions & 7 deletions src/app/auth/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { ConsoleLogger } from 'src/app/_helpers/console-logger';
export class AuthService {
private readonly http = inject(HttpClient);


@Output() logoff = new EventEmitter<string>();
@Output() releaseOldSessions = new EventEmitter<any>();

Expand Down Expand Up @@ -69,6 +68,19 @@ export class AuthService {
catchError(this.handleError));
}

loginWithSAMLToken(token: string): Observable<boolean> {
const headers = { 'Authorization': `Bearer ${token}` };

return this.http.post<any>(`${environment.apiUrl}/admin/saml/auth`, null, { headers })
.pipe(
tap(auth => {
this.doLoginUser(auth.admin, auth.jwt);
this.currentTokenSubject.next(auth.jwt.token);
}),
map(() => true),
catchError(this.handleError));
}

signup(user: { name: string, email: string, password: string, token: string }): Observable<boolean> {
return this.http.post<any>(`${environment.apiUrl}/admin/signup`, user)
.pipe(
Expand Down Expand Up @@ -163,13 +175,13 @@ export class AuthService {
}

private doLoginUser(user: any, tokens: Tokens) {
const loggegWith = this.getLoggedWith(user);
const loggedWith = this.getLoggedWith(user);

this.setUserInfo('name', user.name);
this.setUserInfo('email', user.email);
this.setUserInfo('sessionid', user.id);
this.setUserInfo('avatar', user._avatar);
this.setUserInfo('platform', loggegWith, true);
this.setUserInfo('platform', loggedWith, true);

this.loggedUser = user.email;
this.storeTokens(tokens);
Expand All @@ -181,9 +193,12 @@ export class AuthService {
}

private getLoggedWith(user: any): string {
if (user._gitid) return 'GitHub';
if (user._bitbucketid) return 'Bitbucket';
return 'Switcher API';
switch (user.auth_provider) {
case 'github': return 'GitHub';
case 'bitbucket': return 'Bitbucket';
case 'saml': return 'SAML';
default: return 'Switcher API';
}
}

private getRefreshToken() {
Expand Down
9 changes: 8 additions & 1 deletion src/app/login/login.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,18 @@ <h4 class="card-header">
</button>
}
@if (hasBitbucketIntegration()) {
<button mat-button class="btn-element white" [disabled]="loading" (click)="onBitBucketLogin()">
<button mat-button class="btn-element white" [disabled]="loading" (click)="onBitbucketLogin()">
<img src="assets\bitbucket.svg" style="width: 20px;" alt="Bitbucket" />
<span style="color: black; padding-left: 5px;">Bitbucket</span>
</button>
}
@if (hasSamlAuthEnabled()) {
<hr />
<button mat-button class="btn-element white" [disabled]="loading" (click)="onSamlLogin()">
<img src="assets\saml.svg" style="width: 20px;" alt="SAML" />
<span style="color: black; padding-left: 5px;">SAML</span>
</button>
}
</div>
@if (error) {
<div class="alert alert-danger mt-3 mb-0">{{error}}</div>
Expand Down
38 changes: 34 additions & 4 deletions src/app/login/login.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export class LoginComponent implements OnInit, OnDestroy {
this.router.navigate(['/dashboard']);
}

this.loginWithSAMLToken();

this.route.queryParams.subscribe(params => {
const platform = params['platform'];
const code = params['code'];
Expand All @@ -45,7 +47,7 @@ export class LoginComponent implements OnInit, OnDestroy {
if (platform === 'github') {
this.loginWithGitHub(code);
} else if (platform === 'bitbucket') {
this.loginWithBitBucket(code);
this.loginWithBitbucket(code);
} else {
this.router.navigate(['/']);
}
Expand All @@ -63,8 +65,9 @@ export class LoginComponent implements OnInit, OnDestroy {
get f() { return this.loginForm.controls; }

onSubmit() {
if (this.loginForm.invalid)
if (this.loginForm.invalid) {
return;
}

this.status = '';
this.loading = true;
Expand All @@ -84,11 +87,16 @@ export class LoginComponent implements OnInit, OnDestroy {
window.location.href = `https://github.com/login/oauth/authorize?client_id=${environment.githubApiClientId}`;
}

onBitBucketLogin() {
onBitbucketLogin() {
this.loading = true;
window.location.href = `https://bitbucket.org/site/oauth2/authorize?client_id=${environment.bitbucketApiClientId}&response_type=code`;
}

onSamlLogin() {
this.loading = true;
window.location.href = `${environment.apiUrl}/admin/saml/login`;
}

ngOnDestroy() {
this.unsubscribe.next();
this.unsubscribe.complete();
Expand All @@ -109,6 +117,10 @@ export class LoginComponent implements OnInit, OnDestroy {
hasInternalAuthEnabled(): boolean {
return environment.allowInternalAuth;
}

hasSamlAuthEnabled(): boolean {
return environment.allowSamlAuth;
}

private isAlive(): void {
this.authService.isAlive().pipe(takeUntil(this.unsubscribe))
Expand Down Expand Up @@ -136,7 +148,7 @@ export class LoginComponent implements OnInit, OnDestroy {
});
}

private loginWithBitBucket(code: string) {
private loginWithBitbucket(code: string) {
this.loading = true;

this.authService.loginWithBitBucket(code)
Expand All @@ -147,6 +159,24 @@ export class LoginComponent implements OnInit, OnDestroy {
});
}

private loginWithSAMLToken() {
if (window.location.hash) {
const hashParams = new URLSearchParams(window.location.hash.substring(1));
const token = hashParams.get('token');

if (token) {
this.loading = true;

this.authService.loginWithSAMLToken(token)
.pipe(takeUntil(this.unsubscribe))
.subscribe({
next: success => this.onSuccess(success),
error: error => this.onError(error)
});
}
}
}

private onError(error: any) {
ConsoleLogger.printError(error);
this.error = error;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
.avatar {
border-radius: 100%;
width: 200px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3), 0 6px 20px rgba(0, 0, 0, 0.15);
border: 2px solid rgba(255, 255, 255, 0.1);
-webkit-filter: drop-shadow(1px 1px 5px rgba(0,0,0,0.2));
-ms-filter: "progid:DXImageTransform.Microsoft.Dropshadow(OffX=12, OffY=12, Color='#444')";
filter: "progid:DXImageTransform.Microsoft.Dropshadow(OffX=12, OffY=12, Color='#444')";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,15 +112,12 @@ export class SettingsAccountComponent extends BasicComponent implements OnInit,
}

getPlatformIcon(): string {
if (this.userPlatform === 'Switcher API') {
return "assets\\switcherapi_mark_grey.png";
switch (this.userPlatform) {
case 'Bitbucket': return "assets\\bitbucket.svg";
case 'GitHub': return "assets\\github.svg";
case 'SAML': return "assets\\saml.svg";
default: return "assets\\switcherapi_mark_grey.png";
}

if (this.userPlatform === 'GitHub') {
return "assets\\github.svg";
}

return "assets\\bitbucket.svg";
}

private get accountFormControl() {
Expand All @@ -131,8 +128,9 @@ export class SettingsAccountComponent extends BasicComponent implements OnInit,
this.accountFormControl.name.setValue(this.authService.getUserInfo('name'));
this.userEmail = this.authService.getUserInfo('email');
this.userPlatform = this.authService.getUserInfo('platform');

const avatar = this.authService.getUserInfo('avatar');
this.profileAvatar = avatar || "assets\\switcherapi_mark_grey.png";
this.profileAvatar = avatar || "assets\\switcherapi_mark_icon.png";
}

private loadDomains(): void {
Expand Down
9 changes: 8 additions & 1 deletion src/app/signup/signup.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,18 @@ <h4 class="card-header">
}
@if (hasBitbucketIntegration()) {
<button type="button"
mat-button class="btn-element white" [disabled]="loading" (click)="onBitBucketLogin()">
mat-button class="btn-element white" [disabled]="loading" (click)="onBitbucketLogin()">
<img src="assets\bitbucket.svg" style="width: 20px;" alt="Bitbucket"/>
<span style="color: black; padding-left: 5px;">Bitbucket</span>
</button>
}
@if (hasSamlAuthEnabled()) {
<hr />
<button mat-button class="btn-element white" [disabled]="loading" (click)="onSamlLogin()">
<img src="assets\saml.svg" style="width: 20px;" alt="SAML" />
<span style="color: black; padding-left: 5px;">SAML</span>
</button>
}
</div>
@if (error) {
<div class="alert alert-danger mt-3 mb-0">{{error}}</div>
Expand Down
11 changes: 10 additions & 1 deletion src/app/signup/signup.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,16 @@ export class SignupComponent implements OnInit, OnDestroy {
window.location.href = `https://github.com/login/oauth/authorize?client_id=${environment.githubApiClientId}`;
}

onBitBucketLogin() {
onBitbucketLogin() {
this.loading = true;
window.location.href = `https://bitbucket.org/site/oauth2/authorize?client_id=${environment.bitbucketApiClientId}&response_type=code`;
}

onSamlLogin() {
this.loading = true;
window.location.href = `${environment.apiUrl}/admin/saml/login`;
}

ngOnDestroy() {
this.unsubscribe.next();
this.unsubscribe.complete();
Expand All @@ -110,6 +115,10 @@ export class SignupComponent implements OnInit, OnDestroy {
return environment.allowInternalAuth;
}

hasSamlAuthEnabled(): boolean {
return environment.allowSamlAuth;
}

private submitForm() {
this.authService.signup({
name: this.f.name.value,
Expand Down
27 changes: 24 additions & 3 deletions src/assets/documentation/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@ Switching fast. Adapt everywhere.

### Auth Providers

Switcher API supports multiple auth providers to sign up and sign in such as email/password, GitHub, and Bitbucket.

Follow the steps below to set up your OAuth App in GitHub and Bitbucket.
Switcher API supports multiple auth providers such as email/password-based authentication, SAML 2.0 for Single Sign-On (SSO), or GitHub/Bitbucket OAuth.

#### GitHub OAuth App setup

Expand Down Expand Up @@ -49,6 +47,29 @@ Follow the steps below to set up your OAuth App in GitHub and Bitbucket.
- BIT_OAUTH_CLIENT_SECRET=your_client_secret
8. Update Switcher Management BITBUCKET_CLIENTID environment variable with your_client_id

#### SSO with SAML 2.0 setup

1. Obtain the following information from your Identity Provider (IdP):
- Entry Point URL
- X.509 Certificate
- (Optional) Private Key

2. Update your .env-cmdrc file or ConfigMap/Secret in Kubernetes with the following variables:
- SAML_ENTRY_POINT=your_idp_entry_point_url
- SAML_ISSUER=your_issuer
- SAML_CALLBACK_ENDPOINT_URL=service_provider_callback_endpoint_url
- SAML_REDIRECT_ENDPOINT_URL=web_app_redirect_endpoint_url
- SAML_CERT=your_x509_certificate_base64_encoded
- SAML_PRIVATE_KEY=your_private_key_base64_encoded (if applicable)
- SAML_IDENTIFIER_FORMAT=urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
- SAML_ACCEPTED_CLOCK_SKEW_MS=5000
- SESSION_SECRET=SESSION_SECRET

3. Enable SAML authentication in Switcher Management by setting the environment variable SAML_ENABLE=true

* `service_provider` refers to Switcher API
* `web_app` refers to Switcher Management

### Running Switcher API from Docker Composer manifest file

This option leverages Switcher API and Switcher Management with minimum settings required.
Expand Down
1 change: 1 addition & 0 deletions src/assets/js/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
window["env"]["RELEASE_TIME"] = undefined;
window["env"]["GOOGLE_RECAPTCHA"] = undefined;
window["env"]["ALLOW_INTERNAL_AUTH"] = undefined;
window["env"]["ALLOW_SAML_AUTH"] = undefined;
window["env"]["ALLOW_HOME_VIEW"] = undefined;
window["env"]["GITHUB_CLIENTID"] = undefined;
window["env"]["BITBUCKET_CLIENTID"] = undefined;
Expand Down
1 change: 1 addition & 0 deletions src/assets/js/env.template.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
window["env"]["RELEASE_TIME"] = "${RELEASE_TIME}";
window["env"]["GOOGLE_RECAPTCHA"] = "${GOOGLE_RECAPTCHA}";
window["env"]["ALLOW_INTERNAL_AUTH"] = "${ALLOW_INTERNAL_AUTH}";
window["env"]["ALLOW_SAML_AUTH"] = "${ALLOW_SAML_AUTH}";
window["env"]["ALLOW_HOME_VIEW"] = "${ALLOW_HOME_VIEW}";
window["env"]["GITHUB_CLIENTID"] = "${GITHUB_CLIENTID}";
window["env"]["BITBUCKET_CLIENTID"] = "${BITBUCKET_CLIENTID}";
Expand Down
8 changes: 8 additions & 0 deletions src/assets/saml.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/environments/environment.global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export const environment_global = {
releaseTime: new Date().toUTCString(),
recaptchaPublicKey: '6Lc4fMErAAAAAJSdF2ZRV6HJI_m-nhiDu8Gue18P',
allowInternalAuth: true,
allowSamlAuth: false,
allowHomeView: false,
githubApiClientId: '5745650fe81a1f1f3486',
bitbucketApiClientId: 'My5KxNyU3vNxYdP68G',
Expand Down
1 change: 1 addition & 0 deletions src/environments/environment.prod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const environment = {
releaseTime: getEnv("RELEASE_TIME"),
recaptchaPublicKey: getEnv("GOOGLE_RECAPTCHA"),
allowInternalAuth: getEnv("ALLOW_INTERNAL_AUTH", environment_global.allowInternalAuth),
allowSamlAuth: getEnv("ALLOW_SAML_AUTH", environment_global.allowSamlAuth),
allowHomeView: getEnv("ALLOW_HOME_VIEW", environment_global.allowHomeView),
githubApiClientId: getEnv("GITHUB_CLIENTID"),
bitbucketApiClientId: getEnv("BITBUCKET_CLIENTID"),
Expand Down
1 change: 1 addition & 0 deletions src/environments/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const environment = {
recaptchaPublicKey: environment_global.recaptchaPublicKey,
githubApiClientId: environment_global.githubApiClientId,
allowInternalAuth: environment_global.allowInternalAuth,
allowSamlAuth: environment_global.allowSamlAuth,
allowHomeView: environment_global.allowHomeView,
bitbucketApiClientId: environment_global.bitbucketApiClientId,
teamInviteLink: environment_global.teamInviteLink,
Expand Down