mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-10-03 16:22:05 +00:00
WIP
This commit is contained in:
@@ -0,0 +1,187 @@
|
|||||||
|
import {
|
||||||
|
AuthorizationServiceConfiguration,
|
||||||
|
TokenResponse,
|
||||||
|
} from '@openid/appauth';
|
||||||
|
import {firstValueFrom} from 'rxjs';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AuthActions,
|
||||||
|
AuthProvider,
|
||||||
|
} from './auth.provider';
|
||||||
|
|
||||||
|
describe('AuthProvider', () => {
|
||||||
|
let provider: AuthProvider;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const platform = {
|
||||||
|
is: () => false,
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const configProvider = {} as any;
|
||||||
|
|
||||||
|
provider = new AuthProvider(
|
||||||
|
platform,
|
||||||
|
configProvider,
|
||||||
|
);
|
||||||
|
|
||||||
|
(provider as any).authConfigValue = {
|
||||||
|
server_host: 'https://idp.example.org',
|
||||||
|
client_id: 'test-client',
|
||||||
|
redirect_url: 'https://app.example.org/callback',
|
||||||
|
scopes: 'openid profile',
|
||||||
|
pkce: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
(provider as any).localConfiguration =
|
||||||
|
new AuthorizationServiceConfiguration({
|
||||||
|
authorization_endpoint:
|
||||||
|
'https://idp.example.org/authorize',
|
||||||
|
|
||||||
|
token_endpoint:
|
||||||
|
'https://idp.example.org/token',
|
||||||
|
|
||||||
|
revocation_endpoint:
|
||||||
|
'https://idp.example.org/revoke',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('refreshes the token when the access token is expired', async () => {
|
||||||
|
const expiredToken = new TokenResponse({
|
||||||
|
access_token: 'expired-access-token',
|
||||||
|
refresh_token: 'valid-refresh-token',
|
||||||
|
token_type: 'bearer',
|
||||||
|
});
|
||||||
|
|
||||||
|
spyOn(
|
||||||
|
expiredToken,
|
||||||
|
'isValid',
|
||||||
|
).and.returnValue(false);
|
||||||
|
|
||||||
|
(provider as any).tokenSubject.next(
|
||||||
|
expiredToken,
|
||||||
|
);
|
||||||
|
|
||||||
|
const refreshedToken = new TokenResponse({
|
||||||
|
access_token: 'new-access-token',
|
||||||
|
refresh_token: 'new-refresh-token',
|
||||||
|
token_type: 'bearer',
|
||||||
|
expires_in: '3600',
|
||||||
|
});
|
||||||
|
|
||||||
|
spyOn(
|
||||||
|
refreshedToken,
|
||||||
|
'isValid',
|
||||||
|
).and.returnValue(true);
|
||||||
|
|
||||||
|
const performTokenRequest =
|
||||||
|
spyOn(
|
||||||
|
(provider as any).tokenHandler,
|
||||||
|
'performTokenRequest',
|
||||||
|
).and.resolveTo(
|
||||||
|
refreshedToken,
|
||||||
|
);
|
||||||
|
|
||||||
|
spyOn(
|
||||||
|
(provider as any).storage,
|
||||||
|
'setItem',
|
||||||
|
).and.resolveTo();
|
||||||
|
|
||||||
|
const token =
|
||||||
|
await provider.getValidToken();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
performTokenRequest,
|
||||||
|
).toHaveBeenCalled();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
token.accessToken,
|
||||||
|
).toBe(
|
||||||
|
'new-access-token',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
token.refreshToken,
|
||||||
|
).toBe(
|
||||||
|
'new-refresh-token',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails when the refresh token is expired', async () => {
|
||||||
|
const expiredToken = new TokenResponse({
|
||||||
|
access_token: 'expired-access-token',
|
||||||
|
refresh_token: 'expired-refresh-token',
|
||||||
|
token_type: 'bearer',
|
||||||
|
});
|
||||||
|
|
||||||
|
spyOn(
|
||||||
|
expiredToken,
|
||||||
|
'isValid',
|
||||||
|
).and.returnValue(false);
|
||||||
|
|
||||||
|
(provider as any).tokenSubject.next(
|
||||||
|
expiredToken,
|
||||||
|
);
|
||||||
|
|
||||||
|
spyOn(
|
||||||
|
(provider as any).tokenHandler,
|
||||||
|
'performTokenRequest',
|
||||||
|
).and.returnValue(
|
||||||
|
Promise.reject(
|
||||||
|
new Error('invalid_grant'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expectAsync(
|
||||||
|
provider.getValidToken(),
|
||||||
|
).toBeRejectedWithError(
|
||||||
|
'invalid_grant',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('changes isLoggedIn to false when the refresh token fails', async () => {
|
||||||
|
const expiredToken = new TokenResponse({
|
||||||
|
access_token: 'expired-access-token',
|
||||||
|
refresh_token: 'expired-refresh-token',
|
||||||
|
token_type: 'bearer',
|
||||||
|
});
|
||||||
|
|
||||||
|
spyOn(
|
||||||
|
expiredToken,
|
||||||
|
'isValid',
|
||||||
|
).and.returnValue(false);
|
||||||
|
|
||||||
|
(provider as any).notify({
|
||||||
|
action:
|
||||||
|
AuthActions.LoadTokenFromStorageSuccess,
|
||||||
|
tokenResponse:
|
||||||
|
expiredToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await firstValueFrom(
|
||||||
|
provider.isLoggedIn$,
|
||||||
|
),
|
||||||
|
).toBeTrue();
|
||||||
|
|
||||||
|
spyOn(
|
||||||
|
(provider as any).tokenHandler,
|
||||||
|
'performTokenRequest',
|
||||||
|
).and.returnValue(
|
||||||
|
Promise.reject(
|
||||||
|
new Error('invalid_grant'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expectAsync(
|
||||||
|
provider.getValidToken(),
|
||||||
|
).toBeRejectedWithError(
|
||||||
|
'invalid_grant',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await firstValueFrom(
|
||||||
|
provider.isLoggedIn$,
|
||||||
|
),
|
||||||
|
).toBeFalse();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -554,7 +554,33 @@ export class AuthProvider {
|
|||||||
* requestTokenRefresh() stores the newly returned token
|
* requestTokenRefresh() stores the newly returned token
|
||||||
* and updates tokenSubject via RefreshSuccess.
|
* and updates tokenSubject via RefreshSuccess.
|
||||||
*/
|
*/
|
||||||
await this.requestTokenRefresh();
|
try {
|
||||||
|
await this.requestTokenRefresh();
|
||||||
|
} catch (error) {
|
||||||
|
/*
|
||||||
|
* An invalid_grant response means that the refresh token
|
||||||
|
* is no longer usable, for example because it expired or
|
||||||
|
* was revoked.
|
||||||
|
*
|
||||||
|
* Network errors must not log the user out.
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
error instanceof Error &&
|
||||||
|
error.message.includes(
|
||||||
|
'invalid_grant',
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.authenticatedSubject.next(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.loggedInSubject.next(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
const refreshedToken =
|
const refreshedToken =
|
||||||
this.tokenSubject.value;
|
this.tokenSubject.value;
|
||||||
@@ -875,7 +901,6 @@ export class AuthProvider {
|
|||||||
.completeAuthorizationRequestIfPossible();
|
.completeAuthorizationRequestIfPossible();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async internalEndSessionCallback():
|
private async internalEndSessionCallback():
|
||||||
Promise<void> {
|
Promise<void> {
|
||||||
await this.closeBrowser();
|
await this.closeBrowser();
|
||||||
|
|||||||
@@ -1,70 +1,167 @@
|
|||||||
import {ConfigProvider} from '../config/config.provider';
|
|
||||||
import {IdCardsProvider} from './id-cards.provider';
|
|
||||||
import {HttpClient} from '@angular/common/http';
|
import {HttpClient} from '@angular/common/http';
|
||||||
import {AuthHelperService} from '../auth/auth-helper.service';
|
|
||||||
import {BehaviorSubject, firstValueFrom, of} from 'rxjs';
|
import {BehaviorSubject, firstValueFrom, of} from 'rxjs';
|
||||||
|
|
||||||
|
import {AuthHelperService} from '../auth/auth-helper.service';
|
||||||
|
import {AuthProvider} from '../auth/auth.provider';
|
||||||
|
import {ConfigProvider} from '../config/config.provider';
|
||||||
import {EncryptedStorageProvider} from '../storage/encrypted-storage.provider';
|
import {EncryptedStorageProvider} from '../storage/encrypted-storage.provider';
|
||||||
|
import {IdCardsProvider} from './id-cards.provider';
|
||||||
|
|
||||||
class FakeAuth {
|
class FakeAuth {
|
||||||
isLoggedIn$ = new BehaviorSubject(false);
|
isLoggedIn$ = new BehaviorSubject(false);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
getValidToken = jasmine.createSpy('getValidToken');
|
||||||
getValidToken() {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('IdCards', () => {
|
describe('IdCards', () => {
|
||||||
|
let authHelper: AuthHelperService;
|
||||||
let configProvider: ConfigProvider;
|
let configProvider: ConfigProvider;
|
||||||
let httpClient: HttpClient;
|
let httpClient: HttpClient;
|
||||||
let authHelper: AuthHelperService;
|
let authProvider: AuthProvider;
|
||||||
let encryptedStorageProvider: EncryptedStorageProvider;
|
let encryptedStorageProvider: EncryptedStorageProvider;
|
||||||
let fakeAuth: FakeAuth;
|
let fakeAuth: FakeAuth;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
configProvider = jasmine.createSpyObj('ConfigProvider', ['config']);
|
authHelper = jasmine.createSpyObj(
|
||||||
|
'AuthHelperService',
|
||||||
|
['getUserFromUserInfo'],
|
||||||
|
);
|
||||||
|
|
||||||
|
configProvider = jasmine.createSpyObj(
|
||||||
|
'ConfigProvider',
|
||||||
|
['config'],
|
||||||
|
);
|
||||||
|
|
||||||
configProvider.config = {
|
configProvider.config = {
|
||||||
app: {features: {extern: {idCards: {url: 'http://id-cards.local', authProvider: 'fakeAuth'}}}},
|
app: {
|
||||||
|
features: {
|
||||||
|
extern: {
|
||||||
|
idCards: {
|
||||||
|
url: 'http://id-cards.local',
|
||||||
|
authProvider: 'fakeAuth',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
} as never;
|
} as never;
|
||||||
httpClient = jasmine.createSpyObj('HttpClient', ['get']);
|
|
||||||
|
httpClient = jasmine.createSpyObj(
|
||||||
|
'HttpClient',
|
||||||
|
['get'],
|
||||||
|
);
|
||||||
|
|
||||||
fakeAuth = new FakeAuth();
|
fakeAuth = new FakeAuth();
|
||||||
authHelper = jasmine.createSpyObj('AuthHelperService', ['getProvider']);
|
|
||||||
authHelper.getProvider = jasmine.createSpy().and.returnValue(fakeAuth);
|
authProvider =
|
||||||
encryptedStorageProvider = jasmine.createSpyObj('EncryptedStorageProvider', ['get', 'set', 'delete']);
|
fakeAuth as unknown as AuthProvider;
|
||||||
encryptedStorageProvider.get = jasmine.createSpy().and.resolveTo();
|
|
||||||
encryptedStorageProvider.set = jasmine.createSpy().and.resolveTo();
|
encryptedStorageProvider =
|
||||||
encryptedStorageProvider.delete = jasmine.createSpy().and.resolveTo();
|
jasmine.createSpyObj(
|
||||||
|
'EncryptedStorageProvider',
|
||||||
|
['get', 'set', 'delete'],
|
||||||
|
);
|
||||||
|
|
||||||
|
encryptedStorageProvider.get =
|
||||||
|
jasmine.createSpy().and.resolveTo();
|
||||||
|
|
||||||
|
encryptedStorageProvider.set =
|
||||||
|
jasmine.createSpy().and.resolveTo();
|
||||||
|
|
||||||
|
encryptedStorageProvider.delete =
|
||||||
|
jasmine.createSpy().and.resolveTo();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit undefined if not logged in', async () => {
|
it('should emit undefined if not logged in', async () => {
|
||||||
const provider = new IdCardsProvider(authHelper, configProvider, httpClient, encryptedStorageProvider);
|
const provider = new IdCardsProvider(
|
||||||
expect(await firstValueFrom(provider.getIdCards())).toEqual([]);
|
authHelper,
|
||||||
expect(authHelper.getProvider).toHaveBeenCalledTimes(1);
|
authProvider,
|
||||||
|
configProvider,
|
||||||
|
httpClient,
|
||||||
|
encryptedStorageProvider,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await firstValueFrom(
|
||||||
|
provider.getIdCards(),
|
||||||
|
),
|
||||||
|
).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit network result when logged in', async () => {
|
it('should emit network result when logged in', async () => {
|
||||||
fakeAuth.isLoggedIn$.next(true);
|
fakeAuth.isLoggedIn$.next(true);
|
||||||
httpClient.get = jasmine.createSpy().and.returnValue(of(['abc']));
|
|
||||||
fakeAuth.getValidToken = jasmine.createSpy().and.resolveTo({accessToken: 'fake-token'});
|
httpClient.get =
|
||||||
const provider = new IdCardsProvider(authHelper, configProvider, httpClient, encryptedStorageProvider);
|
jasmine.createSpy().and.returnValue(
|
||||||
expect(await firstValueFrom(provider.getIdCards())).toEqual(['abc' as never]);
|
of(['abc']),
|
||||||
expect(authHelper.getProvider).toHaveBeenCalledTimes(1);
|
);
|
||||||
// eslint-disable-next-line unicorn/no-null
|
|
||||||
expect(httpClient.get).toHaveBeenCalledOnceWith('http://id-cards.local', {
|
fakeAuth.getValidToken.and.resolveTo({
|
||||||
headers: {
|
accessToken: 'fake-token',
|
||||||
Authorization: 'Bearer fake-token',
|
|
||||||
},
|
|
||||||
responseType: 'json',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const provider = new IdCardsProvider(
|
||||||
|
authHelper,
|
||||||
|
authProvider,
|
||||||
|
configProvider,
|
||||||
|
httpClient,
|
||||||
|
encryptedStorageProvider,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await firstValueFrom(
|
||||||
|
provider.getIdCards(),
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
'abc' as never,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// eslint-disable-next-line unicorn/no-null
|
||||||
|
expect(httpClient.get)
|
||||||
|
.toHaveBeenCalledOnceWith(
|
||||||
|
'http://id-cards.local',
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization:
|
||||||
|
'Bearer fake-token',
|
||||||
|
},
|
||||||
|
responseType: 'json',
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should react to logins', async () => {
|
it('should react to logins', async () => {
|
||||||
const provider = new IdCardsProvider(authHelper, configProvider, httpClient, encryptedStorageProvider);
|
const provider = new IdCardsProvider(
|
||||||
const observable = provider.getIdCards();
|
authHelper,
|
||||||
expect(await firstValueFrom(observable)).toEqual([]);
|
authProvider,
|
||||||
httpClient.get = jasmine.createSpy().and.returnValue(of(['abc']));
|
configProvider,
|
||||||
fakeAuth.getValidToken = jasmine.createSpy().and.resolveTo({accessToken: 'fake-token'});
|
httpClient,
|
||||||
|
encryptedStorageProvider,
|
||||||
|
);
|
||||||
|
|
||||||
|
const observable =
|
||||||
|
provider.getIdCards();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await firstValueFrom(observable),
|
||||||
|
).toEqual([]);
|
||||||
|
|
||||||
|
httpClient.get =
|
||||||
|
jasmine.createSpy().and.returnValue(
|
||||||
|
of(['abc']),
|
||||||
|
);
|
||||||
|
|
||||||
|
fakeAuth.getValidToken.and.resolveTo({
|
||||||
|
accessToken: 'fake-token',
|
||||||
|
});
|
||||||
|
|
||||||
fakeAuth.isLoggedIn$.next(true);
|
fakeAuth.isLoggedIn$.next(true);
|
||||||
|
|
||||||
// this is counter-intuitive, but because we unsubscribed above the first value
|
// this is counter-intuitive, but because we unsubscribed above the first value
|
||||||
// will now contain the network result.
|
// will now contain the network result.
|
||||||
expect(await firstValueFrom(observable)).toEqual(['abc' as never]);
|
expect(
|
||||||
|
await firstValueFrom(observable),
|
||||||
|
).toEqual([
|
||||||
|
'abc' as never,
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user