Skip to content

Commit

Permalink
Closes #27 , support EdDSA if the browser supports
Browse files Browse the repository at this point in the history
  • Loading branch information
mrpachara committed Jun 10, 2023
1 parent 6061e1e commit 1714f8d
Show file tree
Hide file tree
Showing 7 changed files with 312 additions and 6 deletions.
218 changes: 218 additions & 0 deletions projects/demo/src/app/app.config.eddsa.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import { ApplicationConfig, inject } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { defer } from 'rxjs';

import {
AccessTokenConfig,
AccessTokenResponse,
AuthorizationCodeConfig,
AuthorizationCodeService,
IdTokenService,
JwkConfig,
Oauth2ClientConfig,
Scopes,
StateActionInfo,
configIdToken,
provideAccessToken,
provideAccessTokenResponseExtractors,
provideAuthorizationCode,
provideJwk,
provideKeyValuePairStorage,
provideOauth2Client,
provideStateAction,
randomString,
withAccessTokenResponseExtractor,
withRenewAccessTokenSource,
withStateActionErrorHandler,
withStateActionHandler,
} from '@mrpachara/ngx-oauth2-access-token';

import { routes } from './app.routes';

const clientConfig: Oauth2ClientConfig = {
name: 'my',
clientId: 'web-app',
accessTokenUrl: 'http://localhost:8080/v2/token',
};

const authorizationCodeConfig: AuthorizationCodeConfig = {
name: 'my',
authorizationCodeUrl: 'http://localhost:8080/authorize/consent',
redirectUri: 'http://localhost:4200/google/authorization',
pkce: 'S256',
// NOTE: For getting refresh token from Google
additionalParams: {
prompt: 'consent',
access_type: 'offline',
},
};

const accessTokenConfig: AccessTokenConfig = {
name: 'my',
};

// NOTE: configIdToken() returns the full configuration
// from the given optional configuration.
const idTokenFullConfig = configIdToken({
providedInAccessToken: true,
});

const jwkConfig: JwkConfig = {
name: 'my',
issuer: 'http://localhost:8080',
jwkSetUrl: 'http://localhost:8080/.well-known/jwk-set.json',
};

type BroadcastData =
| {
type: 'success';
data: AccessTokenResponse;
}
| {
type: 'error';
error: unknown;
};

type BroadcastActionInfo = StateActionInfo<
'broadcast',
{
channel: string;
}
>;

export const appConfig: ApplicationConfig = {
providers: [
// NOTE: withComponentInputBinding() will atomatically bind
// query strings to component inputs.
provideRouter(routes, withComponentInputBinding()),
provideHttpClient(),

// NOTE: The ngx-oauth2-access-token provide functions
provideKeyValuePairStorage(1n), // This is needed now.
provideOauth2Client(clientConfig),
provideAuthorizationCode(authorizationCodeConfig),
provideAccessToken(
accessTokenConfig,
// NOTE: The process for getting the new access token
withRenewAccessTokenSource(() => {
const authorizationCodeService = inject(AuthorizationCodeService);

return defer(
() =>
new Promise<AccessTokenResponse>((resolve, reject) => {
const scopeText = prompt('Input scope');

if (scopeText === null) {
// NOTE: It's safe to throw here because it's out of
// asynchronous process. In asychronous process,
// we have to use reject();
throw new Error('Authorization was canceled.');
}

const scopes = scopeText.split(/\s+/) as Scopes;

const channelName = randomString(8);
const channel = new BroadcastChannel(channelName);

const teardown = () => {
channel.close();
};

channel.addEventListener('message', (ev) => {
const data = ev.data as BroadcastData;

if (data.type === 'success') {
resolve(data.data);
} else {
reject(data.error);
}

teardown();
});

(async () => {
const url =
await authorizationCodeService.fetchAuthorizationCodeUrl(
scopes,
{
// NOTE: The name of action will be performed in the callback URL.
// And the data using in the callback URL.
action: {
name: 'broadcast',
data: {
channel: channelName,
},
} as BroadcastActionInfo,
},
);

open(url, '_blank');
})();
}),
);
}),

// NOTE: The individual extractors can be set here if needed.
// withAccessTokenResponseExtractor(IdTokenService, idTokenFullConfig),
),

// NOTE: Add additional extractors.
provideAccessTokenResponseExtractors(
// NOTE: Add ID token extractor for getting information
// from access token response.
withAccessTokenResponseExtractor(IdTokenService, idTokenFullConfig),
),

// NOTE: The process in callback URL.
provideStateAction(
// NOTE: Use StateActionInfo to determine the types of handler
withStateActionHandler<BroadcastActionInfo>('broadcast', () => {
return async (accessTokenResponse, stateData) => {
const data = stateData.action.data;

const channelName = data.channel;

const channel = new BroadcastChannel(`${channelName}`);
channel.postMessage({
type: 'success',
data: accessTokenResponse,
} as BroadcastData);

channel.close();
close();

// NOTE: If window cannot be closed, we provide success message.
return 'Access token has been set by another process.';
};
}),

// NOTE: When the server return error
withStateActionErrorHandler(() => {
return (err, stateData) => {
const errData: BroadcastData = {
type: 'error',
error: err,
};

if (stateData?.action) {
const broadcastActionInfo = stateData.action as BroadcastActionInfo;

const data = broadcastActionInfo.data;
const channel = new BroadcastChannel(`${data['channel']}`);

const teardown = () => {
channel.close();
// close(); // close windows if needed
};

channel.postMessage(errData);

teardown();
}
};
}),
),
provideJwk(jwkConfig),
],
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import {
JwkBase,
JwkEcBase,
JwkEcdsa,
JwkEddsa,
JwkHmac,
JwkOkpBase,
JwkRsaBase,
JwkRsassa,
JwkSymmetricKeyBase,
JwtHeader,
Provided,
} from '../types';

export function findJwk(
Expand All @@ -31,7 +32,7 @@ export function isJwkSymmetricKey(jwk: JwkBase): jwk is JwkSymmetricKeyBase {
return jwk.kty === 'oct';
}

export function isJwkHmac(jwk: JwkBase): jwk is Provided<JwkHmac, 'alg'> {
export function isJwkHmac(jwk: JwkBase): jwk is JwkHmac {
return (
isJwkSymmetricKey(jwk) &&
(jwk.alg === 'HS256' || jwk.alg === 'HS384' || jwk.alg === 'HS512')
Expand All @@ -42,7 +43,7 @@ export function isJwkRsaKey(jwk: JwkBase): jwk is JwkRsaBase {
return jwk.kty === 'RSA';
}

export function isJwkRsassa(jwk: JwkBase): jwk is Provided<JwkRsassa, 'alg'> {
export function isJwkRsassa(jwk: JwkBase): jwk is JwkRsassa {
return (
isJwkRsaKey(jwk) &&
(jwk.alg === 'RS256' || jwk.alg === 'RS384' || jwk.alg === 'RS512')
Expand All @@ -59,3 +60,11 @@ export function isJwkEcdsa(jwk: JwkBase): jwk is JwkEcdsa {
(jwk.crv === 'P-256' || jwk.crv === 'P-384' || jwk.crv === 'P-512')
);
}

export function isJwkOkpKey(jwk: JwkBase): jwk is JwkOkpBase {
return jwk.kty === 'OKP';
}

export function isJwkEddsa(jwk: JwkBase): jwk is JwkEddsa {
return isJwkOkpKey(jwk) && (jwk.crv === 'Ed25519' || jwk.crv === 'Ed448');
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './jwt-verifiers/jwt-hmac.verifier';
export * from './jwt-verifiers/jwt-rsassa.verifier';
export * from './jwt-verifiers/jwt-ecdsa.verifier';
export * from './jwt-verifiers/jwt-eddsa.verifier';
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';

import { JwtEddsaVerifier } from './jwt-eddsa.verifier';

describe('JwtEddsaVerifier', () => {
let service: JwtEddsaVerifier;

beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(JwtEddsaVerifier);
});

it('should be created', () => {
expect(service).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Injectable, isDevMode } from '@angular/core';

import { isJwkEddsa } from '../functions';
import { JwkBase, JwtInfo, JwtVerifier, Provided } from '../types';

@Injectable({
providedIn: 'root',
})
export class JwtEddsaVerifier implements JwtVerifier {
async verify(
jwtInfo: Provided<JwtInfo, 'signature'>,
jwk: JwkBase,
): Promise<boolean | undefined> {
if (isJwkEddsa(jwk)) {
try {
const key = await crypto.subtle.importKey(
'jwk',
jwk,
{
name: jwk.crv,
},
true,
['verify'],
);
const encoder = new TextEncoder();

return await crypto.subtle.verify(
{
name: key.algorithm.name,
},
key,
jwtInfo.signature,
encoder.encode(jwtInfo.content),
);
} catch (err) {
if (isDevMode()) {
console.warn(err);
}

return undefined;
}
}

return undefined;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { InjectionToken, inject } from '@angular/core';

import {
JwtEcdsaVerifier,
JwtEddsaVerifier,
JwtHmacVerifier,
JwtRsassaVerifier,
} from '../jwt-verifiers';
Expand All @@ -25,6 +26,7 @@ export const DEFAULT_JWT_VERIFIERS = new InjectionToken<JwtVerifier[]>(
inject(JwtHmacVerifier),
inject(JwtRsassaVerifier),
inject(JwtEcdsaVerifier),
inject(JwtEddsaVerifier),
],
},
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export type JwkHashBase = JwkSymmetricKeyBase & {
/** HMAC - Hash-based Message Authentication Codes Algorithm */
export type JwkHmac<SHA extends '256' | '384' | '512' = '256' | '384' | '512'> =
JwkHashBase & {
alg?: `HS${SHA}`;
alg: `HS${SHA}`;
};

/** Asymmetric Key */
Expand All @@ -81,10 +81,10 @@ export type JwkRsaBase = JwkAsymmetricKeyBase & {
export type JwkRsassa<
SHA extends '256' | '384' | '512' = '256' | '384' | '512',
> = JwkRsaBase & {
alg?: `RS${SHA}`;
alg: `RS${SHA}`;
};

/** EC - Elliptic Curve Key */
/** EC Key - Elliptic Curve Key */
export type JwkEcBase = JwkAsymmetricKeyBase & {
kty: 'EC';
crv: string;
Expand All @@ -99,6 +99,20 @@ export type JwkEcdsa<P extends '256' | '384' | '512' = '256' | '384' | '512'> =
y: string;
};

/** OKP Key - Octet Key Pair */
export type JwkOkpBase = JwkAsymmetricKeyBase & {
kty: 'OKP';
crv: string;
x: string;
};

/** EdDSA - Edwards-Curve Digital Signature Algorithm */
export type JwkEddsa<ED extends '25519' | '448' = '25519' | '448'> =
JwkOkpBase & {
alg?: 'EdDSA';
crv: `Ed${ED}`;
};

/** JWK Set */
export type JwkSet = {
keys: JwkBase[];
Expand Down

0 comments on commit 1714f8d

Please sign in to comment.