web/satellite: add consent screen for oauth

When an application wants to interact with resources on behalf of
an end-user, it needs to be granted access. In OAuth, this is done
when a user submits the consent screen.

Change-Id: Id838772f76999f63f5c9dbdda0995697b41c123a
This commit is contained in:
Mya 2022-02-14 14:06:35 -06:00 committed by mya
parent 4f196dd39e
commit 5cebbdee03
10 changed files with 847 additions and 20 deletions

View File

@ -20,15 +20,7 @@ func GenAccessGrant(satelliteNodeURL, apiKey, encryptionPassphrase, projectID st
return "", err
}
id, err := uuid.FromString(projectID)
if err != nil {
return "", err
}
const concurrency = 8
salt := sha256.Sum256(id[:])
key, err := encryption.DeriveRootKey([]byte(encryptionPassphrase), salt[:], "", concurrency)
key, err := DeriveRootKey(encryptionPassphrase, projectID)
if err != nil {
return "", err
}
@ -47,3 +39,16 @@ func GenAccessGrant(satelliteNodeURL, apiKey, encryptionPassphrase, projectID st
}
return accessString, nil
}
// DeriveRootKey derives the root key portion of the access grant.
func DeriveRootKey(encryptionPassphrase, projectID string) (*storj.Key, error) {
id, err := uuid.FromString(projectID)
if err != nil {
return nil, err
}
const concurrency = 8
salt := sha256.Sum256(id[:])
return encryption.DeriveRootKey([]byte(encryptionPassphrase), salt[:], "", concurrency)
}

View File

@ -295,6 +295,7 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, oidc
router.Handle("/oauth/v2/authorize", server.withAuth(http.HandlerFunc(oidc.AuthorizeUser))).Methods(http.MethodPost)
router.Handle("/oauth/v2/tokens", server.ipRateLimiter.Limit(http.HandlerFunc(oidc.Tokens))).Methods(http.MethodPost)
router.Handle("/oauth/v2/userinfo", server.ipRateLimiter.Limit(http.HandlerFunc(oidc.UserInfo))).Methods(http.MethodGet)
router.Handle("/oauth/v2/clients/{id}", server.withAuth(http.HandlerFunc(oidc.GetClient))).Methods(http.MethodGet)
fs := http.FileServer(http.Dir(server.config.StaticDir))
router.PathPrefix("/static/").Handler(server.brotliMiddleware(http.StripPrefix("/static", fs)))

View File

@ -7,15 +7,19 @@
package main
import (
"crypto/aes"
"crypto/sha256"
"encoding/json"
"syscall/js"
"github.com/zeebo/errs"
"storj.io/common/base58"
console "storj.io/storj/satellite/console/consolewasm"
)
func main() {
js.Global().Set("deriveAndEncryptRootKey", deriveAndEncryptRootKey())
js.Global().Set("generateAccessGrant", generateAccessGrant())
js.Global().Set("setAPIKeyPermission", setAPIKeyPermission())
js.Global().Set("newPermission", newPermission())
@ -23,6 +27,38 @@ func main() {
<-make(chan bool)
}
// deriveAndEncryptRootKey derives the root key portion of the access grant and then encrypts it using the provided aes
// key. To ensure the key used in encryption is a proper length, we take a sha256 of the provided key and use it instead
// of using the key directly. Then we base58 encode the result and return it to the caller.
func deriveAndEncryptRootKey() js.Func {
return js.FuncOf(responseHandler(func(this js.Value, args []js.Value) (interface{}, error) {
if len(args) < 3 {
return nil, errs.New("not enough arguments. Need 3, but only %d supplied. The order of arguments are: encryption passphrase, project ID, and AES Key.", len(args))
}
encryptionPassphrase := args[0].String()
projectSalt := args[1].String()
aesKey := args[2].String()
rootKey, err := console.DeriveRootKey(encryptionPassphrase, projectSalt)
if err != nil {
return nil, err
}
key := sha256.Sum256([]byte(aesKey))
cipher, err := aes.NewCipher(key[:])
if err != nil {
return nil, err
}
value := *(rootKey.Raw())
out := make([]byte, len(value))
cipher.Encrypt(out, value[:])
return base58.Encode(out), nil
}))
}
// generateAccessGrant creates a new access grant with the provided api key and encryption passphrase.
func generateAccessGrant() js.Func {
return js.FuncOf(responseHandler(func(this js.Value, args []js.Value) (interface{}, error) {

View File

@ -4,7 +4,9 @@
package oidc
import (
"database/sql"
"encoding/json"
"errors"
"net/http"
"strings"
"time"
@ -12,6 +14,7 @@ import (
"github.com/go-oauth2/oauth2/v4"
"github.com/go-oauth2/oauth2/v4/manage"
"github.com/go-oauth2/oauth2/v4/server"
"github.com/gorilla/mux"
"github.com/spacemonkeygo/monkit/v3"
"go.uber.org/zap"
@ -31,9 +34,10 @@ func NewEndpoint(
) *Endpoint {
manager := manage.NewManager()
clientStore := oidcService.ClientStore()
tokenStore := oidcService.TokenStore()
manager.MapClientStorage(oidcService.ClientStore())
manager.MapClientStorage(clientStore)
manager.MapTokenStorage(tokenStore)
manager.MapAuthorizeGenerate(&UUIDAuthorizeGenerate{})
@ -59,10 +63,11 @@ func NewEndpoint(
// externalAddress _should_ end with a '/' suffix based on the calling path
return &Endpoint{
tokenStore: tokenStore,
service: service,
server: svr,
log: log,
clientStore: clientStore,
tokenStore: tokenStore,
service: service,
server: svr,
log: log,
config: ProviderConfig{
Issuer: externalAddress,
AuthURL: externalAddress + "oauth/v2/authorize",
@ -77,11 +82,12 @@ func NewEndpoint(
//
// architecture: Endpoint
type Endpoint struct {
tokenStore oauth2.TokenStore
service *console.Service
server *server.Server
log *zap.Logger
config ProviderConfig
clientStore oauth2.ClientStore
tokenStore oauth2.TokenStore
service *console.Service
server *server.Server
log *zap.Logger
config ProviderConfig
}
// WellKnownConfiguration renders the identity provider configuration that points clients to various endpoints.
@ -178,6 +184,33 @@ func (e *Endpoint) UserInfo(w http.ResponseWriter, r *http.Request) {
}
}
// GetClient returns non-sensitive information about an OAuthClient. This information is used to initially verify client
// applications who are requesting information on behalf of a user.
func (e *Endpoint) GetClient(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
vars := mux.Vars(r)
client, err := e.clientStore.GetByID(ctx, vars["id"])
switch {
case errors.Is(err, sql.ErrNoRows):
http.NotFound(w, r)
return
case err != nil:
http.Error(w, "", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(client)
if err != nil {
e.log.Error("failed to encode oauth client", zap.Error(err))
}
}
// ProviderConfig defines a subset of elements used by OIDC to auto-discover endpoints.
type ProviderConfig struct {
Issuer string `json:"issuer"`

View File

@ -0,0 +1,36 @@
// Copyright (C) 2022 Storj Labs, Inc.
// See LICENSE for copying information.
import {HttpClient} from "@/utils/httpClient";
import {ErrorTooManyRequests} from "@/api/errors/ErrorTooManyRequests";
export interface OAuthClient {
id: string;
redirectURL: string;
appName: string;
appLogoURL: string;
}
export class OAuthClientsAPI {
private readonly http: HttpClient = new HttpClient();
private readonly ROOT_PATH: string = '/oauth/v2/clients';
public async get(id: string): Promise<OAuthClient> {
const path = `${this.ROOT_PATH}/${id}`;
const response = await this.http.get(path);
if (response.ok) {
return response.json().then((body) => body as OAuthClient);
}
switch (response.status) {
case 400:
throw new Error('Invalid request for OAuth client');
case 404:
throw new Error('OAuth client was not found');
case 429:
throw new ErrorTooManyRequests('API rate limit exceeded');
}
throw new Error('Failed to lookup oauth client');
}
}

View File

@ -78,6 +78,7 @@ export default class HeaderlessInput extends Vue {
private type: string = this.textType;
private isPasswordShown = false;
@Prop({default: ''})
protected value = '';
@Prop({default: ''})

View File

@ -52,6 +52,7 @@ import { NavigationLink } from '@/types/navigation';
import { MetaUtils } from "@/utils/meta";
const ActivateAccount = () => import('@/views/ActivateAccount.vue');
const AuthorizeArea = () => import('@/views/AuthorizeArea.vue');
const DashboardArea = () => import('@/views/DashboardArea.vue');
const ForgotPassword = () => import('@/views/ForgotPassword.vue');
const LoginArea = () => import('@/views/LoginArea.vue');
@ -72,6 +73,7 @@ export abstract class RouteConfig {
public static Activate = new NavigationLink('/activate', 'Activate');
public static ForgotPassword = new NavigationLink('/forgot-password', 'Forgot Password');
public static ResetPassword = new NavigationLink('/password-recovery', 'Reset Password');
public static Authorize = new NavigationLink('/oauth/v2/authorize', 'Authorize')
public static Account = new NavigationLink('/account', 'Account');
public static ProjectDashboard = new NavigationLink('/project-dashboard', 'Dashboard');
public static NewProjectDashboard = new NavigationLink('/new-project-dashboard', 'Dashboard ');
@ -135,6 +137,7 @@ export const notProjectRelatedRoutes = [
RouteConfig.Activate.name,
RouteConfig.ForgotPassword.name,
RouteConfig.ResetPassword.name,
RouteConfig.Authorize.name,
RouteConfig.Billing.name,
RouteConfig.BillingHistory.name,
RouteConfig.DepositHistory.name,
@ -175,6 +178,11 @@ export const router = new Router({
name: RouteConfig.ResetPassword.name,
component: ResetPassword,
},
{
path: RouteConfig.Authorize.path,
name: RouteConfig.Authorize.name,
component: AuthorizeArea,
},
{
path: RouteConfig.Root.path,
meta: {

View File

@ -0,0 +1,694 @@
// Copyright (C) 2022 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="authorize-area">
<div class="authorize-area__logo-wrapper">
<LogoIcon class="logo" @click="location.reload()" />
</div>
<div class="authorize-area__content-area">
<div v-if="requestErr" class="authorize-area__content-area__container">
<p>{{ requestErr }}</p>
</div>
<div v-else class="authorize-area__content-area__container">
<p class="authorize-area__content-area__client-app-logo">
<img :alt="client.appName" :src="client.appLogoURL">
</p>
<p class="authorize-area__content-area__client-app">
{{ client.appName }} would like permission to:
</p>
<div class="authorize-area__permissions-area">
<div class="authorize-area__permissions-area__container">
<p class="authorize-area__permissions-area__header">Verify your Storj Identity</p>
<p>Access and view your account info.</p>
</div>
<div class="authorize-area__permissions-area__container">
<p class="authorize-area__permissions-area__header">Sync data to Storj DCS</p>
<p>Automatically send updates to:</p>
<div class="authorize-area__input-wrapper">
<HeaderlessInput
label="Project"
role-description="project"
:error="projectErr"
:options-list="Object.keys(projects)"
@setData="setProject"
/>
</div>
<div class="authorize-area__input-wrapper">
<HeaderlessInput
label="Bucket"
role-description="bucket"
:error="bucketErr"
:value="selectedBucketName"
:options-list="buckets"
@setData="setBucket"
/>
<div v-if="!bucketExists" class="info-box">
<p class="info-box__message">
This bucket will be created.
</p>
</div>
</div>
<div class="authorize-area__input-wrapper">
<HeaderlessInput
label="Passphrase"
role-description="passphrase"
placeholder="Passphrase"
:error="passphraseErr"
is-password="true"
@setData="setPassphrase"
/>
</div>
</div>
<div class="authorize-area__permissions-area__container">
<p class="authorize-area__permissions-area__container__header">Perform the following actions</p>
<p>{{ actions }} objects.</p>
</div>
</div>
<form method="post">
<input v-model="oauthData['client_id']" type="hidden" name="client_id">
<input v-model="oauthData['redirect_uri']" type="hidden" name="redirect_uri">
<input v-model="oauthData['response_type']" type="hidden" name="response_type">
<input v-model="oauthData['state']" type="hidden" name="state">
<input v-model="scope" type="hidden" name="scope">
<input class="authorize-area__content-area__container__button" :class="{ 'disabled-button': !valid }" type="submit" :disabled="!valid" value="Authorize">
<p class="authorize-area__content-area__container__cancel" @click.prevent="onDeny">Cancel</p>
</form>
</div>
</div>
</div>
</template>
<script lang="ts">
import {Component, Vue} from 'vue-property-decorator';
import HeaderlessInput from '@/components/common/HeaderlessInput.vue';
import LogoIcon from '@/../static/images/logo.svg';
import {Validator} from '@/utils/validation';
import {RouteConfig} from '@/router';
import querystring, {ParsedUrlQueryInput} from 'querystring';
import {BUCKET_ACTIONS} from '@/store/modules/buckets';
import {PROJECTS_ACTIONS} from '@/store/modules/projects';
import {USER_ACTIONS} from '@/store/modules/users';
import {Project} from '@/types/projects';
import {ErrorUnauthorized} from '@/api/errors/ErrorUnauthorized';
import {APP_STATE_ACTIONS} from '@/utils/constants/actionNames';
import {AppState} from '@/utils/constants/appStateEnum';
import {ACCESS_GRANTS_ACTIONS} from '@/store/modules/accessGrants';
import {OAuthClient, OAuthClientsAPI} from '@/api/oauthClients';
const oauthClientsAPI = new OAuthClientsAPI();
// @vue/component
@Component({
components: {
HeaderlessInput,
LogoIcon,
},
})
export default class Authorize extends Vue {
private requestErr = '';
private oauthData: ParsedUrlQueryInput = {};
private clientKey = '';
private client: OAuthClient = {
id: '',
redirectURL: '',
appName: '',
appLogoURL: '',
};
private projects: Record<string, Project> = {};
private buckets: Array<string> = [];
private selectedProjectID = '';
private selectedBucketName = '';
private providedPassphrase = '';
private scope = '';
private valid = false;
private projectErr = '';
private bucketErr = '';
private passphraseErr = '';
private actions = '';
private bucketExists = false;
private worker: Worker;
private async ensureLogin(): Promise<void> {
try {
await this.$store.dispatch(USER_ACTIONS.GET);
} catch (error) {
if (!(error instanceof ErrorUnauthorized)) {
await this.$store.dispatch(APP_STATE_ACTIONS.CHANGE_STATE, AppState.ERROR);
await this.$notify.error(error.message);
}
const query = querystring.stringify(this.oauthData) as string;
const path = `${RouteConfig.Authorize.path}?${query}#${this.clientKey}`;
await this.$router.push(`${RouteConfig.Login.path}?return_url=${encodeURIComponent(path)}`);
return;
}
}
private async ensureWorker(): Promise<void> {
try {
await this.$store.dispatch(ACCESS_GRANTS_ACTIONS.STOP_ACCESS_GRANTS_WEB_WORKER);
await this.$store.dispatch(ACCESS_GRANTS_ACTIONS.SET_ACCESS_GRANTS_WEB_WORKER);
} catch (error) {
await this.$notify.error(`Unable to set access grants wizard. ${error.message}`);
return;
}
this.worker = this.$store.state.accessGrantsModule.accessGrantsWebWorker;
this.worker.onerror = (error: ErrorEvent) => this.$notify.error(error.message);
}
private async verifyClientConfiguration(): Promise<void> {
const clientID = 'client_id' in this.oauthData ? `${this.oauthData['client_id']}` : '';
const redirectURL = 'redirect_uri' in this.oauthData ? `${this.oauthData['redirect_uri']}` : '';
const state = 'state' in this.oauthData ? `${this.oauthData['state']}` : '';
const responseType = 'response_type' in this.oauthData ? `${this.oauthData['response_type']}` : '';
const scope = 'scope' in this.oauthData ? `${this.oauthData['scope']}` : '';
if (!clientID || !redirectURL) {
this.requestErr = 'Both client_id and redirect_uri must be provided.';
return;
}
let client: OAuthClient;
try {
client = await oauthClientsAPI.get(clientID);
} catch (e) {
this.requestErr = e.message;
return;
}
let err: { [key: string]: string } | null = null;
if (!state || !responseType || !scope) {
err = {
error_description: 'The request is missing a required parameter (state, response_type, or scope).',
};
} else if (!this.clientKey) {
err = {
error_description: 'An encryption key must be provided in the fragment of the request.',
};
} else if (!redirectURL.startsWith(client.redirectURL)) {
err = {
error_description: 'The provided redirect url does not match the one in our system.',
};
}
if (err) {
location.href = `${redirectURL}?${querystring.stringify(err)}`;
return
}
this.client = client;
// initialize the form
this.setBucket(slugify(this.client.appName));
this.actions = formatObjectPermissions(scope);
}
private async loadProjects(): Promise<void> {
await this.$store.dispatch(PROJECTS_ACTIONS.FETCH);
const projects = {};
for (const project of this.$store.getters.projects) {
projects[project.name] = project;
}
this.projects = projects;
}
/**
* Lifecycle hook after initial render.
* Makes activated banner visible on successful account activation.
*/
public async mounted(): Promise<void> {
this.oauthData = this.$route.query as ParsedUrlQueryInput;
this.clientKey = this.$route.hash ? this.$route.hash.substr(1) : "";
await this.ensureLogin();
await this.ensureWorker();
await this.verifyClientConfiguration();
if (this.requestErr) {
return
}
await this.loadProjects();
}
public async setProject(value: string): Promise<void> {
if (!this.projects[value]) {
this.projectErr = 'project does not exist';
return
}
await this.$store.dispatch(PROJECTS_ACTIONS.SELECT, this.projects[value].id);
await this.$store.dispatch(BUCKET_ACTIONS.FETCH_ALL_BUCKET_NAMES);
this.selectedProjectID = this.$store.getters.selectedProject.id;
this.buckets = this.$store.state.bucketUsageModule.allBucketNames.sort();
this.setBucket(this.selectedBucketName);
}
public setBucket(value: string): void {
this.selectedBucketName = value;
this.bucketExists = this.selectedProjectID.length == 0 || value.length == 0 || this.buckets.includes(value);
this.setScope();
}
public setPassphrase(value: string): void {
this.providedPassphrase = value;
this.setScope();
}
public async setScope(): Promise<void> {
if (!this.validate()) {
return;
}
this.worker.postMessage({
'type': 'DeriveAndEncryptRootKey',
'passphrase': this.providedPassphrase,
'projectID': this.selectedProjectID,
'aesKey': this.clientKey,
});
const event: MessageEvent = await new Promise(resolve => this.worker.onmessage = resolve);
if (event.data.error) {
await this.$notify.error(event.data.error);
return;
}
const scope = this.oauthData['scope'],
project = this.selectedProjectID,
bucket = this.selectedBucketName,
cubbyhole = event.data.value;
this.scope = `${scope} project:${project} bucket:${bucket} cubbyhole:${cubbyhole}`;
}
public async onDeny(): Promise<void> {
location.href = `${this.oauthData['redirect_uri']}?${querystring.stringify({
error_description: 'The resource owner or authorization server denied the request',
})}`;
}
private validate(): boolean {
this.projectErr = '';
this.bucketErr = '';
this.passphraseErr = '';
if (this.selectedProjectID == '') {
this.projectErr = 'Missing project.';
}
if (!Validator.bucketName(this.selectedBucketName)) {
this.bucketErr = 'Name must contain only lowercase latin characters, numbers, a hyphen or a period';
}
if (this.providedPassphrase == '') {
this.passphraseErr = 'A passphrase must be provided.';
}
this.valid = this.projectErr == '' &&
this.bucketErr == '' &&
this.passphraseErr == '';
return this.valid;
}
}
const validPerms = {
'list': true,
'read': true,
'write': true,
'delete': true,
};
function slugify(name: string): string {
name = name.toLowerCase();
name = name.replace(/\s+/g, "-");
return name;
}
function formatObjectPermissions(scope: string): string {
const scopes = scope.split(" ");
const perms: string[] = [];
for (const scope of scopes) {
if (scope.startsWith("object:")) {
const perm = scope.substr("object:".length);
if (validPerms[perm]) {
perms.push(perm);
}
}
}
perms.sort();
if (perms.length == 0) {
return "";
} else if (perms.length == 1) {
return perms[0];
} else if (perms.length == 2) {
return `${perms[0]} and ${perms[1]}`;
}
return `${perms.slice(0, perms.length - 1).join(", ")}, and ${perms[perms.length - 1]}`;
}
</script>
<style scoped lang="scss">
.authorize-area {
display: flex;
flex-direction: column;
font-family: 'font_regular', sans-serif;
background-color: #f5f6fa;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
min-height: 100%;
overflow-y: scroll;
.info-box {
background-color: #e9f3ff;
border-radius: 6px;
padding: 20px;
margin-top: 25px;
width: 100%;
box-sizing: border-box;
&.error {
background-color: #fff9f7;
border: 1px solid #f84b00;
}
&__header {
display: flex;
align-items: center;
&__label {
font-family: 'font_bold', sans-serif;
font-size: 16px;
color: #1b2533;
margin-left: 15px;
}
}
&__message {
font-size: 16px;
color: #1b2533;
}
}
&__permissions-area {
background-color: #fafafb;
border: 1px solid #d8dee3;
padding: 10px 9px 10px 24px;
border-radius: 8px;
&__header {
font-size: 18px;
margin-bottom: 4px;
}
&__container {
padding: 24px 0;
border-bottom: 1px solid #d8dee3;
p {
line-height: 24px;
vertical-align: middle;
}
&:last-of-type {
border-bottom: none;
}
}
}
&__logo-wrapper {
text-align: center;
margin: 70px 0;
}
&__divider {
margin: 0 20px;
height: 22px;
width: 2px;
background-color: #acbace;
}
&__input-wrapper {
margin-top: 20px;
padding-right: 24px;
width: 100%;
box-sizing: border-box;
}
&__expand {
display: flex;
align-items: center;
cursor: pointer;
position: relative;
&__value {
font-size: 16px;
line-height: 21px;
color: #acbace;
margin-right: 10px;
font-family: 'font_regular', sans-serif;
font-weight: 700;
}
&__dropdown {
position: absolute;
top: 35px;
left: 0;
background-color: #fff;
z-index: 1000;
border: 1px solid #c5cbdb;
box-shadow: 0 8px 34px rgba(161, 173, 185, 0.41);
border-radius: 6px;
min-width: 250px;
&__item {
display: flex;
align-items: center;
justify-content: flex-start;
padding: 12px 25px;
font-size: 14px;
line-height: 20px;
color: #7e8b9c;
cursor: pointer;
text-decoration: none;
&__name {
font-family: 'font_bold', sans-serif;
margin-left: 15px;
font-size: 14px;
line-height: 20px;
color: #7e8b9c;
}
&:hover {
background-color: #f2f2f6;
}
}
}
}
&__content-area {
background-color: #f5f6fa;
padding: 0 20px;
margin-bottom: 50px;
display: flex;
flex-direction: column;
align-items: center;
border-radius: 20px;
box-sizing: border-box;
&__activation-banner {
padding: 20px;
background-color: rgba(39, 174, 96, 0.1);
border: 1px solid #27ae60;
color: #27ae60;
border-radius: 6px;
width: 570px;
margin-bottom: 30px;
&__message {
font-size: 16px;
line-height: 21px;
margin: 0;
}
}
&__client-app-logo {
text-align: center;
margin-bottom: 60px;
}
&__client-app {
text-align: center;
font-size: 22px;
font-weight: bold;
margin-bottom: 16px;
}
&__container {
display: flex;
flex-direction: column;
padding: 60px 80px;
background-color: #fff;
width: 610px;
border-radius: 20px;
box-sizing: border-box;
margin-bottom: 20px;
&__title-area {
display: flex;
justify-content: space-between;
align-items: center;
&__title {
font-size: 24px;
line-height: 49px;
letter-spacing: -0.100741px;
color: #252525;
font-family: 'font_bold', sans-serif;
font-weight: 800;
}
&__satellite {
font-size: 16px;
line-height: 21px;
color: #848484;
}
}
&__button {
font-family: 'font_regular', sans-serif;
font-weight: 700;
margin-top: 40px;
display: flex;
justify-content: center;
align-items: center;
background-color: #376fff;
border-radius: 50px;
color: #fff;
cursor: pointer;
width: 100%;
height: 48px;
&:hover {
background-color: #0059d0;
}
}
&__cancel {
align-self: center;
font-size: 16px;
line-height: 21px;
color: #0068dc;
text-align: center;
margin-top: 30px;
cursor: pointer;
}
&__recovery {
font-size: 16px;
line-height: 19px;
color: #0068dc;
cursor: pointer;
margin-top: 20px;
text-align: center;
width: 100%;
}
}
&__footer-item {
margin-top: 30px;
font-size: 14px;
}
}
}
.logo {
cursor: pointer;
}
.disabled,
.disabled-button {
pointer-events: none;
color: #acb0bc;
}
.disabled-button {
background-color: #dadde5;
border-color: #dadde5;
}
@media screen and (max-width: 750px) {
.authorize-area {
&__content-area {
&__container {
width: 100%;
padding: 60px;
}
}
&__expand {
&__dropdown {
left: -200px;
}
}
}
}
@media screen and (max-width: 414px) {
.authorize-area {
&__logo-wrapper {
margin: 40px;
}
&__content-area {
padding: 0;
&__container {
padding: 0 20px 20px 20px;
background: transparent;
}
}
}
}
</style>

View File

@ -147,6 +147,7 @@ export default class Login extends Vue {
private readonly auth: AuthHttpApi = new AuthHttpApi();
public readonly forgotPasswordPath: string = RouteConfig.ForgotPassword.path;
public returnURL: string = RouteConfig.ProjectDashboard.path;
public isActivatedBannerShown = false;
public isActivatedError = false;
public isMFARequired = false;
@ -178,6 +179,8 @@ export default class Login extends Vue {
public mounted(): void {
this.isActivatedBannerShown = !!this.$route.query.activated;
this.isActivatedError = this.$route.query.activated === 'false';
this.returnURL = this.$route.query.return_url as string || this.returnURL;
}
/**
@ -310,7 +313,7 @@ export default class Login extends Vue {
await this.$store.dispatch(APP_STATE_ACTIONS.CHANGE_STATE, AppState.LOADING);
this.isLoading = false;
await this.$router.push(RouteConfig.ProjectDashboard.path);
await this.$router.push(this.returnURL);
}
/**

View File

@ -29,6 +29,16 @@ self.onmessage = async function (event) {
self.postMessage(new Error(e.message))
}
break;
case 'DeriveAndEncryptRootKey':
{
const passphrase = data.passphrase;
const projectID = data.projectID;
const aesKey = data.aesKey;
result = self.deriveAndEncryptRootKey(passphrase, projectID, aesKey);
self.postMessage(result);
}
break;
case 'GenerateAccess':
apiKey = data.apiKey;