diff --git a/packages/ionic/assets/sso/success/index.html b/packages/ionic/assets/sso/success/index.html
new file mode 100644
index 0000000000..7737fb406d
--- /dev/null
+++ b/packages/ionic/assets/sso/success/index.html
@@ -0,0 +1,51 @@
+
+
+
+
+ Success
+
+
+
+
+
You are authenticated.
+
Please return to your terminal. You may close this window.
+
+
+
diff --git a/packages/ionic/src/commands/git/remote.ts b/packages/ionic/src/commands/git/remote.ts
index 0d1a8bff62..a55bb11825 100644
--- a/packages/ionic/src/commands/git/remote.ts
+++ b/packages/ionic/src/commands/git/remote.ts
@@ -30,7 +30,7 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://dashboard.ionicframework.com')}
const token = this.env.session.getUserToken();
const proId = await this.project.requireProId();
- const appClient = new AppClient({ token, client: this.env.client });
+ const appClient = new AppClient(token, this.env);
const app = await appClient.load(proId);
if (!app.repo_url) {
diff --git a/packages/ionic/src/commands/link.ts b/packages/ionic/src/commands/link.ts
index 306eb99594..1eb01cafcc 100644
--- a/packages/ionic/src/commands/link.ts
+++ b/packages/ionic/src/commands/link.ts
@@ -220,13 +220,13 @@ ${chalk.cyan('[2]')}: ${chalk.bold('https://ionicframework.com/support/request')
private async getAppClient() {
const { AppClient } = await import('../lib/app');
const token = this.env.session.getUserToken();
- return new AppClient({ token, client: this.env.client });
+ return new AppClient(token, this.env);
}
private async getUserClient() {
const { UserClient } = await import('../lib/user');
const token = this.env.session.getUserToken();
- return new UserClient({ token, client: this.env.client });
+ return new UserClient(token, this.env);
}
async lookUpApp(proId: string): Promise {
@@ -243,7 +243,8 @@ ${chalk.cyan('[2]')}: ${chalk.bold('https://ionicframework.com/support/request')
async createApp({ name }: { name: string; }, runinfo: CommandInstanceInfo): Promise {
const appClient = await this.getAppClient();
- const app = await appClient.create({ name });
+ const org_id = this.env.config.get('org.id');
+ const app = await appClient.create({ name, org_id });
await this.linkApp(app, runinfo);
diff --git a/packages/ionic/src/commands/login.ts b/packages/ionic/src/commands/login.ts
index a0e5525eaa..31a6d4d9d7 100644
--- a/packages/ionic/src/commands/login.ts
+++ b/packages/ionic/src/commands/login.ts
@@ -1,10 +1,9 @@
-import { validators } from '@ionic/cli-framework';
+import { OptionGroup, validators } from '@ionic/cli-framework';
import chalk from 'chalk';
-import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata, CommandPreRun } from '../definitions';
+import { CommandLineInputs, CommandLineOptions, CommandMetadata, CommandPreRun } from '../definitions';
import { Command } from '../lib/command';
import { FatalException } from '../lib/errors';
-import { runCommand } from '../lib/executor';
import { generateUUID } from '../lib/utils/uuid';
export class LoginCommand extends Command implements CommandPreRun {
@@ -37,14 +36,25 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/support/request')
{
name: 'password',
summary: 'Your password',
- validators: [validators.required],
+ // this is a hack since sso is hidden, no need to make password not required for it
+ validators: process.argv.includes('--sso') ? [] : [validators.required],
private: true,
},
],
+ options: [
+ {
+ name: 'sso',
+ type: Boolean,
+ summary: 'Open a window to log in with the SSO provider associated with your email',
+ groups: [OptionGroup.Hidden],
+ },
+ ],
};
}
async preRun(inputs: CommandLineInputs, options: CommandLineOptions): Promise {
+ const sso = !!options['sso'];
+
if (options['email'] || options['password']) {
throw new FatalException(
`${chalk.green('email')} and ${chalk.green('password')} are command arguments, not options. Please try this:\n` +
@@ -52,20 +62,32 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/support/request')
);
}
+ const askForEmail = !inputs[0];
+ const askForPassword = !sso && !inputs[1];
+
if (this.env.session.isLoggedIn()) {
- const extra = !inputs[0] || !inputs[1] ? 'Prompting for new credentials.' : 'Attempting login.';
const email = this.env.config.get('user.email');
- this.env.log.warn(`You are already logged in${email ? ' as ' + chalk.bold(email) : ''}! ${this.env.flags.interactive ? extra : ''}`);
+
+ const extra = askForEmail || askForPassword
+ ? (this.env.flags.interactive ? `Prompting for new credentials.\n\nUse ${chalk.yellow('Ctrl+C')} to cancel and remain logged in.` : '')
+ : 'You will be logged out beforehand.';
+
+ this.env.log.warn(
+ 'You will be logged out.\n' +
+ `You are already logged in${email ? ' as ' + chalk.bold(email) : ''}! ${extra}`
+ );
+ this.env.log.nl();
} else {
- this.env.log.msg(
- `Log into your Ionic Pro account\n` +
- `If you don't have one yet, create yours by running: ${chalk.green(`ionic signup`)}\n`
+ this.env.log.info(
+ `Log into your Ionic Pro account!\n` +
+ `If you don't have one yet, create yours by running: ${chalk.green(`ionic signup`)}`
);
+ this.env.log.nl();
}
// TODO: combine with promptToLogin ?
- if (!inputs[0]) {
+ if (askForEmail) {
const email = await this.env.prompt({
type: 'input',
name: 'email',
@@ -76,7 +98,7 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/support/request')
inputs[0] = email;
}
- if (!inputs[1]) {
+ if (askForPassword) {
const password = await this.env.prompt({
type: 'password',
name: 'password',
@@ -89,17 +111,27 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/support/request')
}
}
- async run(inputs: CommandLineInputs, options: CommandLineOptions, runinfo: CommandInstanceInfo): Promise {
+ async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise {
const [ email, password ] = inputs;
+ const sso = !!options['sso'];
if (this.env.session.isLoggedIn()) {
- this.env.log.msg('Logging you out.');
- await runCommand(runinfo, ['logout']);
+ await this.env.session.logout();
this.env.config.set('tokens.telemetry', generateUUID());
}
- await this.env.session.login(email, password);
+ if (sso) {
+ this.env.log.info(
+ `Ionic Pro SSO Login\n` +
+ `During this process, a browser window will open to authenticate you with the identity provider for ${chalk.green(email)}. Please leave this process running until authentication is complete.`
+ );
+ this.env.log.nl();
+
+ await this.env.session.ssoLogin(email);
+ } else {
+ await this.env.session.login(email, password);
+ }
- this.env.log.ok('You are logged in!');
+ this.env.log.ok(chalk.green.bold('You are logged in!'));
}
}
diff --git a/packages/ionic/src/commands/start.ts b/packages/ionic/src/commands/start.ts
index 4bf46d14cc..14665a2b52 100644
--- a/packages/ionic/src/commands/start.ts
+++ b/packages/ionic/src/commands/start.ts
@@ -270,7 +270,7 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/docs/cli/starters
if (proId) {
const { AppClient } = await import('../lib/app');
const token = this.env.session.getUserToken();
- const appClient = new AppClient({ token, client: this.env.client });
+ const appClient = new AppClient(token, this.env);
const tasks = this.createTaskChain();
tasks.next(`Looking up app ${chalk.green(proId)}`);
const app = await appClient.load(proId);
diff --git a/packages/ionic/src/definitions.ts b/packages/ionic/src/definitions.ts
index fa280ae42e..5544ae6042 100644
--- a/packages/ionic/src/definitions.ts
+++ b/packages/ionic/src/definitions.ts
@@ -326,6 +326,7 @@ export type NamespaceLocateResult = ζframework.NamespaceLocateResult;
+ ssoLogin(email: string): Promise;
tokenLogin(token: string): Promise;
logout(): Promise;
@@ -385,6 +386,7 @@ export interface ConfigFile {
'git.host'?: string;
'git.port'?: number;
'git.setup'?: boolean;
+ 'org.id'?: string;
'user.id'?: number;
'user.email'?: string;
'tokens.user'?: string;
diff --git a/packages/ionic/src/guards.ts b/packages/ionic/src/guards.ts
index 570e01e6b6..451f08171b 100644
--- a/packages/ionic/src/guards.ts
+++ b/packages/ionic/src/guards.ts
@@ -28,6 +28,8 @@ import {
User,
} from './definitions';
+import { AuthConnection } from './lib/auth';
+
export const INTEGRATION_NAMES: IntegrationName[] = ['capacitor', 'cordova'];
export function isCommand(cmd: any): cmd is ICommand {
@@ -219,6 +221,14 @@ export function isLoginResponse(res: APIResponse): res is Response {
return isAPIResponseSuccess(res) && isLogin(res.data);
}
+export function isAuthConnection(connection: any): connection is AuthConnection {
+ return connection && typeof connection.uuid === 'string';
+}
+
+export function isAuthConnectionResponse(res: APIResponse): res is Response {
+ return isAPIResponseSuccess(res) && isAuthConnection(res.data);
+}
+
export function isUser(user: any): user is User {
return user
&& typeof user.id === 'number'
diff --git a/packages/ionic/src/lib/app.ts b/packages/ionic/src/lib/app.ts
index 6d612d8113..e1dc06f99c 100644
--- a/packages/ionic/src/lib/app.ts
+++ b/packages/ionic/src/lib/app.ts
@@ -15,27 +15,22 @@ export function formatName(app: Pick) {
export interface AppClientDeps {
readonly client: IClient;
- readonly token: string;
}
export interface AppCreateDetails {
- name: string;
+ readonly name: string;
+ readonly org_id?: string;
}
export class AppClient extends ResourceClient implements ResourceClientLoad, ResourceClientCreate, ResourceClientPaginate {
- protected client: IClient;
- protected token: string;
-
- constructor({ client, token }: AppClientDeps) {
+ constructor(readonly token: string, readonly e: AppClientDeps) {
super();
- this.client = client;
- this.token = token;
}
async load(id: string): Promise {
- const { req } = await this.client.make('GET', `/apps/${id}`);
+ const { req } = await this.e.client.make('GET', `/apps/${id}`);
this.applyAuthentication(req, this.token);
- const res = await this.client.do(req);
+ const res = await this.e.client.do(req);
if (!isAppResponse(res)) {
throw createFatalAPIFormat(req, res);
@@ -44,11 +39,11 @@ export class AppClient extends ResourceClient implements ResourceClientLoad
return res.data;
}
- async create({ name }: AppCreateDetails): Promise {
- const { req } = await this.client.make('POST', '/apps');
+ async create(details: AppCreateDetails): Promise {
+ const { req } = await this.e.client.make('POST', '/apps');
this.applyAuthentication(req, this.token);
- req.send({ name });
- const res = await this.client.do(req);
+ req.send(details);
+ const res = await this.e.client.do(req);
if (!isAppResponse(res)) {
throw createFatalAPIFormat(req, res);
@@ -58,9 +53,9 @@ export class AppClient extends ResourceClient implements ResourceClientLoad
}
paginate(args: Partial>> = {}): IPaginator, PaginatorState> {
- return this.client.paginate({
+ return this.e.client.paginate({
reqgen: async () => {
- const { req } = await this.client.make('GET', '/apps');
+ const { req } = await this.e.client.make('GET', '/apps');
this.applyAuthentication(req, this.token);
return { req };
},
@@ -70,17 +65,17 @@ export class AppClient extends ResourceClient implements ResourceClientLoad
}
async createAssociation(id: string, association: { repoId: number; type: AssociationType; branches: string[] }): Promise {
- const { req } = await this.client.make('POST', `/apps/${id}/repository`);
+ const { req } = await this.e.client.make('POST', `/apps/${id}/repository`);
req
.set('Authorization', `Bearer ${this.token}`)
- .send({
- repository_id: association.repoId,
- type: association.type,
- branches: association.branches,
- });
+ .send({
+ repository_id: association.repoId,
+ type: association.type,
+ branches: association.branches,
+ });
- const res = await this.client.do(req);
+ const res = await this.e.client.do(req);
if (!isAppAssociationResponse(res)) {
throw createFatalAPIFormat(req, res);
@@ -90,7 +85,7 @@ export class AppClient extends ResourceClient implements ResourceClientLoad
}
async deleteAssociation(id: string): Promise {
- const { req } = await this.client.make('DELETE', `/apps/${id}/repository`);
+ const { req } = await this.e.client.make('DELETE', `/apps/${id}/repository`);
req
.set('Authorization', `Bearer ${this.token}`)
diff --git a/packages/ionic/src/lib/auth.ts b/packages/ionic/src/lib/auth.ts
new file mode 100644
index 0000000000..f437b657ae
--- /dev/null
+++ b/packages/ionic/src/lib/auth.ts
@@ -0,0 +1,38 @@
+import { IClient, ResourceClientLoad } from '../definitions';
+import { isAuthConnectionResponse } from '../guards';
+
+import { ResourceClient, createFatalAPIFormat } from './http';
+
+export interface AuthConnection {
+ readonly uuid: string;
+}
+
+export interface AuthClientDeps {
+ readonly client: IClient;
+}
+
+export class AuthClient extends ResourceClient {
+ readonly connections: AuthConnectionClient;
+
+ constructor(readonly e: AuthClientDeps) {
+ super();
+ this.connections = new AuthConnectionClient(e);
+ }
+}
+
+export class AuthConnectionClient extends ResourceClient implements ResourceClientLoad {
+ constructor(readonly e: AuthClientDeps) {
+ super();
+ }
+
+ async load(email: string): Promise {
+ const { req } = await this.e.client.make('GET', `/auth/connections/${email}`);
+ const res = await this.e.client.do(req);
+
+ if (!isAuthConnectionResponse(res)) {
+ throw createFatalAPIFormat(req, res);
+ }
+
+ return res.data;
+ }
+}
diff --git a/packages/ionic/src/lib/doctor/ailments/index.ts b/packages/ionic/src/lib/doctor/ailments/index.ts
index adf0430d0d..45e968a411 100644
--- a/packages/ionic/src/lib/doctor/ailments/index.ts
+++ b/packages/ionic/src/lib/doctor/ailments/index.ts
@@ -159,7 +159,7 @@ export class GitConfigInvalid extends Ailment {
}
const token = this.session.getUserToken();
- const appClient = new AppClient({ token, client: this.client });
+ const appClient = new AppClient(token, { client: this.client });
const app = await appClient.load(proId);
if (app.repo_url !== remote) {
diff --git a/packages/ionic/src/lib/session.ts b/packages/ionic/src/lib/session.ts
index 1302888d57..64688cf02f 100644
--- a/packages/ionic/src/lib/session.ts
+++ b/packages/ionic/src/lib/session.ts
@@ -12,27 +12,22 @@ export interface SessionDeps {
}
export class BaseSession {
- protected config: IConfig;
- protected client: IClient;
-
- constructor({ config, client }: SessionDeps) {
- this.config = config;
- this.client = client;
- }
+ constructor(readonly e: SessionDeps) {}
async logout(): Promise {
- this.config.unset('user.id');
- this.config.unset('user.email');
- this.config.unset('tokens.user');
- this.config.set('git.setup', false);
+ this.e.config.unset('org.id');
+ this.e.config.unset('user.id');
+ this.e.config.unset('user.email');
+ this.e.config.unset('tokens.user');
+ this.e.config.set('git.setup', false);
}
isLoggedIn(): boolean {
- return typeof this.config.get('tokens.user') === 'string';
+ return typeof this.e.config.get('tokens.user') === 'string';
}
getUser(): { id: number; } {
- const userId = this.config.get('user.id');
+ const userId = this.e.config.get('user.id');
if (!userId) {
throw new SessionException(
@@ -45,7 +40,7 @@ export class BaseSession {
}
getUserToken(): string {
- const userToken = this.config.get('tokens.user');
+ const userToken = this.e.config.get('tokens.user');
if (!userToken) {
throw new SessionException(
@@ -60,11 +55,11 @@ export class BaseSession {
export class ProSession extends BaseSession implements ISession {
async login(email: string, password: string): Promise {
- const { req } = await this.client.make('POST', '/login');
+ const { req } = await this.e.client.make('POST', '/login');
req.send({ email, password, source: 'cli' });
try {
- const res = await this.client.do(req);
+ const res = await this.e.client.do(req);
if (!isLoginResponse(res)) {
const data = res.data;
@@ -81,13 +76,13 @@ export class ProSession extends BaseSession implements ISession {
const { token, user } = res.data;
- if (this.config.get('user.id') !== user.id) { // User changed
+ if (this.e.config.get('user.id') !== user.id) { // User changed
await this.logout();
}
- this.config.set('user.id', user.id);
- this.config.set('user.email', email);
- this.config.set('tokens.user', token);
+ this.e.config.set('user.id', user.id);
+ this.e.config.set('user.email', email);
+ this.e.config.set('tokens.user', token);
} catch (e) {
if (isSuperAgentError(e) && (e.response.status === 401 || e.response.status === 403)) {
throw new SessionException('Incorrect email or password.');
@@ -97,22 +92,37 @@ export class ProSession extends BaseSession implements ISession {
}
}
- async tokenLogin(token: string) {
+ async ssoLogin(email: string): Promise {
+ const { AuthClient } = await import('./auth');
+ const { Auth0OAuth2Flow } = await import('./sso');
+
+ const authClient = new AuthClient(this.e);
+ const { uuid: connection } = await authClient.connections.load(email);
+
+ const flow = new Auth0OAuth2Flow({ email, connection }, this.e);
+ const token = await flow.run();
+
+ await this.tokenLogin(token);
+
+ this.e.config.set('org.id', connection);
+ }
+
+ async tokenLogin(token: string): Promise {
const { UserClient } = await import('./user');
- const userClient = new UserClient({ client: this.client, token });
+ const userClient = new UserClient(token, this.e);
try {
const user = await userClient.loadSelf();
const user_id = user.id;
- if (this.config.get('user.id') !== user_id) { // User changed
+ if (this.e.config.get('user.id') !== user_id) { // User changed
await this.logout();
}
- this.config.set('user.id', user_id);
- this.config.set('user.email', user.email);
- this.config.set('tokens.user', token);
+ this.e.config.set('user.id', user_id);
+ this.e.config.set('user.email', user.email);
+ this.e.config.set('tokens.user', token);
} catch (e) {
if (isSuperAgentError(e) && (e.response.status === 401 || e.response.status === 403)) {
throw new SessionException('Invalid auth token.');
diff --git a/packages/ionic/src/lib/sso.ts b/packages/ionic/src/lib/sso.ts
new file mode 100644
index 0000000000..c0cf84f547
--- /dev/null
+++ b/packages/ionic/src/lib/sso.ts
@@ -0,0 +1,179 @@
+import { readFile } from '@ionic/utils-fs';
+import { isPortAvailable } from '@ionic/utils-network';
+import * as crypto from 'crypto';
+import * as http from 'http';
+import * as path from 'path';
+import * as qs from 'querystring';
+
+import { ASSETS_DIRECTORY } from '../constants';
+import { IClient } from '../definitions';
+
+const REDIRECT_PORT = 8123;
+const REDIRECT_HOST = 'localhost';
+
+export interface AuthorizationParameters {
+ [key: string]: string;
+}
+
+export interface TokenParameters {
+ [key: string]: string;
+}
+
+export interface OAuth2FlowOptions {
+ readonly authorizationUrl: string;
+ readonly tokenUrl: string;
+ readonly clientId: string;
+ readonly redirectHost?: string;
+ readonly redirectPort?: number;
+}
+
+export interface OAuth2FlowDeps {
+ readonly client: IClient;
+}
+
+export abstract class OAuth2Flow {
+ readonly authorizationUrl: string;
+ readonly tokenUrl: string;
+ readonly clientId: string;
+ readonly redirectHost: string;
+ readonly redirectPort: number;
+
+ constructor({ authorizationUrl, tokenUrl, clientId, redirectHost = REDIRECT_HOST, redirectPort = REDIRECT_PORT }: OAuth2FlowOptions, readonly e: OAuth2FlowDeps) {
+ this.authorizationUrl = authorizationUrl;
+ this.tokenUrl = tokenUrl;
+ this.clientId = clientId;
+ this.redirectHost = redirectHost;
+ this.redirectPort = redirectPort;
+ }
+
+ get redirectUrl(): string {
+ return `http://${this.redirectHost}:${this.redirectPort}`;
+ }
+
+ async run(): Promise {
+ const opn = await import('opn');
+
+ const verifier = this.generateVerifier();
+ const challenge = this.generateChallenge(verifier);
+
+ const authorizationParams = this.generateAuthorizationParameters(challenge);
+ const authorizationUrl = `${this.authorizationUrl}?${qs.stringify(authorizationParams)}`;
+
+ await opn(authorizationUrl, { wait: false });
+
+ const authorizationCode = await this.getAuthorizationCode();
+ const token = await this.getAccessToken(authorizationCode, verifier);
+
+ return token;
+ }
+
+ protected abstract generateAuthorizationParameters(challenge: string): AuthorizationParameters;
+ protected abstract generateTokenParameters(authorizationCode: string, verifier: string): TokenParameters;
+
+ protected async getSuccessHtml(): Promise {
+ const p = path.resolve(ASSETS_DIRECTORY, 'sso', 'success', 'index.html');
+ const contents = await readFile(p, { encoding: 'utf8' });
+
+ return contents;
+ }
+
+ protected async getAuthorizationCode(): Promise {
+ if (!(await isPortAvailable(this.redirectPort))) {
+ throw new Error(`Cannot start local server. Port ${this.redirectPort} is in use.`);
+ }
+
+ const successHtml = await this.getSuccessHtml();
+
+ return new Promise((resolve, reject) => {
+ const server = http.createServer((req, res) => {
+ if (req.url) {
+ const params = qs.parse(req.url.substring(req.url.indexOf('?') + 1));
+
+ if (params.code) {
+ res.writeHead(200, { 'Content-Type': 'text/html' });
+ res.end(successHtml);
+ req.socket.destroy();
+ server.close();
+
+ resolve(params.code);
+ }
+
+ // TODO, timeout, error handling
+ }
+ });
+
+ server.listen(this.redirectPort, this.redirectHost);
+ });
+ }
+
+ protected async getAccessToken(authorizationCode: string, verifier: string): Promise {
+ const params = this.generateTokenParameters(authorizationCode, verifier);
+ const { req } = await this.e.client.make('POST', this.tokenUrl);
+
+ const res = await req.send(params);
+
+ return res.body.access_token;
+ }
+
+ protected generateVerifier(): string {
+ return this.base64URLEncode(crypto.randomBytes(32));
+ }
+
+ protected generateChallenge(verifier: string): string {
+ return this.base64URLEncode(crypto.createHash('sha256').update(verifier).digest());
+ }
+
+ protected base64URLEncode(buffer: Buffer) {
+ return buffer.toString('base64')
+ .replace(/\+/g, '-')
+ .replace(/\//g, '_')
+ .replace(/=/g, '');
+ }
+}
+
+const AUTHORIZATION_URL = 'https://auth.ionicframework.com/authorize';
+const TOKEN_URL = 'https://auth.ionicframework.com/oauth/token';
+const CLIENT_ID = '0kTF4wm74vppjImr11peCjQo2PIQDS3m';
+const API_AUDIENCE = 'https://api.ionicjs.com';
+
+export interface Auth0OAuth2FlowOptions extends Partial {
+ readonly email: string;
+ readonly connection: string;
+ readonly audience?: string;
+}
+
+export class Auth0OAuth2Flow extends OAuth2Flow {
+ readonly email: string;
+ readonly audience: string;
+ readonly connection: string;
+
+ constructor({ email, connection, audience = API_AUDIENCE, authorizationUrl = AUTHORIZATION_URL, tokenUrl = TOKEN_URL, clientId = CLIENT_ID, ...options }: Auth0OAuth2FlowOptions, readonly e: OAuth2FlowDeps) {
+ super({ authorizationUrl, tokenUrl, clientId, ...options }, e);
+ this.email = email;
+ this.connection = connection;
+ this.audience = audience;
+ }
+
+ protected generateAuthorizationParameters(challenge: string): AuthorizationParameters {
+ return {
+ audience: this.audience,
+ scope: 'openid profile email offline_access',
+ response_type: 'code',
+ connection: this.connection,
+ client_id: this.clientId,
+ code_challenge: challenge,
+ code_challenge_method: 'S256',
+ redirect_uri: this.redirectUrl,
+ };
+ }
+
+ protected generateTokenParameters(code: string, verifier: string): TokenParameters {
+ return {
+ grant_type: 'authorization_code',
+ client_id: this.clientId,
+ code_verifier: verifier,
+ code,
+ redirect_uri: this.redirectUrl,
+ };
+ }
+}
diff --git a/packages/ionic/src/lib/user.ts b/packages/ionic/src/lib/user.ts
index 24733ac8b3..861667b5c1 100644
--- a/packages/ionic/src/lib/user.ts
+++ b/packages/ionic/src/lib/user.ts
@@ -5,24 +5,18 @@ import { ResourceClient, TokenPaginator, createFatalAPIFormat } from './http';
export interface UserClientDeps {
readonly client: IClient;
- readonly token: string;
}
export class UserClient extends ResourceClient implements ResourceClientLoad {
- protected client: IClient;
- protected token: string;
-
- constructor({ client, token }: UserClientDeps) {
+ constructor(readonly token: string, readonly e: UserClientDeps) {
super();
- this.client = client;
- this.token = token;
}
async load(id: number, modifiers?: ResourceClientRequestModifiers): Promise {
- const { req } = await this.client.make('GET', `/users/${id}`);
+ const { req } = await this.e.client.make('GET', `/users/${id}`);
this.applyAuthentication(req, this.token);
this.applyModifiers(req, modifiers);
- const res = await this.client.do(req);
+ const res = await this.e.client.do(req);
if (!isUserResponse(res)) {
throw createFatalAPIFormat(req, res);
@@ -32,9 +26,9 @@ export class UserClient extends ResourceClient implements ResourceClientLoad