Merge branch 'travis/token-security'

This commit is contained in:
Travis Ralston 2019-03-27 08:30:12 -06:00
commit 0c0429df46
13 changed files with 124 additions and 6 deletions

View File

@ -1,10 +1,12 @@
import { GET, Path, QueryParam } from "typescript-rest";
import { GET, Path, POST, QueryParam } from "typescript-rest";
import { ScalarService } from "../scalar/ScalarService";
import config from "../../config";
import { ApiError } from "../ApiError";
import { MatrixLiteClient } from "../../matrix/MatrixLiteClient";
import { CURRENT_VERSION } from "../../version";
import { getFederationConnInfo } from "../../matrix/helpers";
import UserScalarToken from "../../db/models/UserScalarToken";
import { Cache, CACHE_SCALAR_ACCOUNTS } from "../../MemoryCache";
interface DimensionVersionResponse {
version: string;
@ -20,6 +22,9 @@ interface DimensionConfigResponse {
federationHostname: string;
clientServerUrl: string;
};
sessionInfo: {
numTokens: number;
};
}
/**
@ -82,6 +87,9 @@ export class AdminService {
federationHostname: fedInfo.hostname,
clientServerUrl: config.homeserver.clientServerUrl,
},
sessionInfo: {
numTokens: await UserScalarToken.count(),
},
};
}
@ -95,4 +103,23 @@ export class AdminService {
resolvedServer: await getFederationConnInfo(serverName),
};
}
@POST
@Path("sessions/logout/all")
public async logoutAll(@QueryParam("scalar_token") scalarToken: string): Promise<any> {
await AdminService.validateAndGetAdminTokenOwner(scalarToken);
// Clear the cache first to hopefully invalidate a bunch of them
Cache.for(CACHE_SCALAR_ACCOUNTS).clear();
const tokens = await UserScalarToken.all();
for (const token of tokens) {
await token.destroy();
}
// Clear it again because the delete loop can be slow
Cache.for(CACHE_SCALAR_ACCOUNTS).clear();
return {};
}
}

View File

@ -49,7 +49,11 @@ export class ScalarService {
@POST
@Path("register")
public async register(request: RegisterRequest): Promise<ScalarRegisterResponse> {
public async register(request: RegisterRequest, @QueryParam("v") apiVersion: string): Promise<ScalarRegisterResponse> {
if (apiVersion !== "1.1") {
throw new ApiError(401, "Invalid API version.");
}
const mxClient = new MatrixOpenIdClient(<OpenId>request);
const mxUserId = await mxClient.getUserId();
@ -95,7 +99,11 @@ export class ScalarService {
@GET
@Path("account")
public async getAccount(@QueryParam("scalar_token") scalarToken: string): Promise<ScalarAccountResponse> {
public async getAccount(@QueryParam("scalar_token") scalarToken: string, @QueryParam("v") apiVersion: string): Promise<ScalarAccountResponse> {
if (apiVersion !== "1.1") {
throw new ApiError(401, "Invalid API version.");
}
const userId = await ScalarService.getTokenOwner(scalarToken);
return {user_id: userId};
}

View File

@ -3,6 +3,7 @@ import { ScalarRegisterResponse } from "../models/ScalarResponses";
import * as request from "request";
import { LogService } from "matrix-js-snippets";
import Upstream from "../db/models/Upstream";
import { SCALAR_API_VERSION } from "../utils/common-constants";
export class ScalarClient {
constructor(private upstream: Upstream) {
@ -14,6 +15,7 @@ export class ScalarClient {
request({
method: "POST",
url: this.upstream.scalarUrl + "/register",
qs: {v: SCALAR_API_VERSION},
json: openId,
}, (err, res, _body) => {
if (err) {

View File

@ -0,0 +1 @@
export const SCALAR_API_VERSION = "1.1";

View File

@ -28,6 +28,16 @@
Utility User ID: {{ config.homeserver.userId }}
</div>
</div>
<div class="row">
<div class="col-md-4">
<strong>Sessions</strong><br />
Tokens registered: {{ config.sessionInfo.numTokens }}<br />
<button class="btn btn-danger btn-sm" type="button" (click)="logoutAll()">
Logout Everyone
</button>
</div>
</div>
</div>
</my-ibox>
</div>

View File

@ -1,6 +1,12 @@
import { Component } from "@angular/core";
import { AdminApiService } from "../../shared/services/admin/admin-api.service";
import { FE_DimensionConfig } from "../../shared/models/admin-responses";
import { ToasterService } from "angular2-toaster";
import { Modal, overlayConfigFactory } from "ngx-modialog";
import {
AdminLogoutConfirmationDialogComponent,
LogoutConfirmationDialogContext
} from "./logout-confirmation/logout-confirmation.component";
@Component({
templateUrl: "./home.component.html",
@ -11,10 +17,26 @@ export class AdminHomeComponent {
public isLoading = true;
public config: FE_DimensionConfig;
constructor(adminApi: AdminApiService) {
constructor(private adminApi: AdminApiService,
private toaster: ToasterService,
private modal: Modal) {
adminApi.getConfig().then(config => {
this.config = config;
this.isLoading = false;
});
}
public logoutAll(): void {
this.modal.open(AdminLogoutConfirmationDialogComponent, overlayConfigFactory({
isBlocking: true,
}, LogoutConfirmationDialogContext)).result.then(() => {
this.adminApi.logoutAll().then(() => {
this.toaster.pop("success", "Everyone has been logged out");
this.config.sessionInfo.numTokens = 0;
}).catch(err => {
console.error(err);
this.toaster.pop("error", "Error logging everyone out");
});
});
}
}

View File

@ -0,0 +1,20 @@
<div class="dialog">
<div class="dialog-header">
<h4>Logout confirmation</h4>
</div>
<div class="dialog-content">
<p>
Logging everyone out will disable all known login tokens for Dimension and upstream integration managers.
Most clients will automatically re-register for a login token behind the scenes, similar to how a login token
was first acquired.
</p>
</div>
<div class="dialog-footer">
<button type="button" (click)="dialog.dismiss()" title="close" class="btn btn-secondary btn-sm">
<i class="far fa-times-circle"></i> Cancel
</button>
<button type="button" (click)="dialog.close()" title="logout everyone" class="btn btn-danger btn-sm">
<i class="far fa-times-circle"></i> Logout Everyone
</button>
</div>
</div>

View File

@ -0,0 +1,15 @@
import { Component } from "@angular/core";
import { DialogRef, ModalComponent } from "ngx-modialog";
import { BSModalContext } from "ngx-modialog/plugins/bootstrap";
export class LogoutConfirmationDialogContext extends BSModalContext {
}
@Component({
templateUrl: "./logout-confirmation.component.html",
styleUrls: ["./logout-confirmation.component.scss"],
})
export class AdminLogoutConfirmationDialogComponent implements ModalComponent<LogoutConfirmationDialogContext> {
constructor(public dialog: DialogRef<LogoutConfirmationDialogContext>) {}
}

View File

@ -109,6 +109,7 @@ import { SlackBridgeConfigComponent } from "./configs/bridge/slack/slack.bridge.
import { AdminSlackBridgeManageSelfhostedComponent } from "./admin/bridges/slack/manage-selfhosted/manage-selfhosted.component";
import { AdminSlackBridgeComponent } from "./admin/bridges/slack/slack.component";
import { AdminSlackApiService } from "./shared/services/admin/admin-slack-api.service";
import { AdminLogoutConfirmationDialogComponent } from "./admin/home/logout-confirmation/logout-confirmation.component";
import { ReauthExampleWidgetWrapperComponent } from "./widget-wrappers/reauth-example/reauth-example.component";
import { ManagerTestWidgetWrapperComponent } from "./widget-wrappers/manager-test/manager-test.component";
@ -200,6 +201,7 @@ import { ManagerTestWidgetWrapperComponent } from "./widget-wrappers/manager-tes
SlackBridgeConfigComponent,
AdminSlackBridgeManageSelfhostedComponent,
AdminSlackBridgeComponent,
AdminLogoutConfirmationDialogComponent,
ReauthExampleWidgetWrapperComponent,
ManagerTestWidgetWrapperComponent,
@ -254,6 +256,7 @@ import { ManagerTestWidgetWrapperComponent } from "./widget-wrappers/manager-tes
AdminGitterBridgeManageSelfhostedComponent,
AdminAddCustomBotComponent,
AdminSlackBridgeManageSelfhostedComponent,
AdminLogoutConfirmationDialogComponent,
]
})
export class AppModule {

View File

@ -10,6 +10,9 @@ export interface FE_DimensionConfig {
federationHostname: string;
clientServerUrl: string;
};
sessionInfo: {
numTokens: number;
};
}
export interface FE_DimensionVersion {

View File

@ -20,4 +20,8 @@ export class AdminApiService extends AuthedApi {
public getVersion(): Promise<FE_DimensionVersion> {
return this.authedGet("/api/v1/dimension/admin/version").map(r => r.json()).toPromise();
}
public logoutAll(): Promise<any> {
return this.authedPost("/api/v1/dimension/admin/sessions/logout/all").map(r => r.json()).toPromise();
}
}

View File

@ -6,6 +6,7 @@ import {
FE_ScalarRegisterResponse
} from "../../models/scalar-server-responses";
import { AuthedApi } from "../authed-api";
import { SCALAR_API_VERSION } from "../../../../../src/utils/common-constants";
@Injectable()
export class ScalarServerApiService extends AuthedApi {
@ -18,10 +19,12 @@ export class ScalarServerApiService extends AuthedApi {
}
public getAccount(): Promise<FE_ScalarAccountResponse> {
return this.authedGet("/api/v1/scalar/account").map(res => res.json()).toPromise();
return this.authedGet("/api/v1/scalar/account", {v: SCALAR_API_VERSION}).map(res => res.json()).toPromise();
}
public register(openId: FE_ScalarOpenIdRequestBody): Promise<FE_ScalarRegisterResponse> {
return this.http.post("/api/v1/scalar/register", openId).map(res => res.json()).toPromise();
return this.http.post("/api/v1/scalar/register", openId, {
params: {v: SCALAR_API_VERSION},
}).map(res => res.json()).toPromise();
}
}