Skip to content

Commit

Permalink
feat: http session cookie (#1032)
Browse files Browse the repository at this point in the history
## Description
support http session cookie. 
- take the session expiration from JWT response's top level attribute,
and not from the session jwt (which might be on cookie)
- improve high level sdk state management - to not consider session
token existence as `is authenticated` state, but rather manage this
separately
 
still missing tests and some other considerations (see comment in vue
sdk)
  • Loading branch information
asafshen authored Mar 3, 2025
1 parent d5c906b commit 0cd7ee3
Show file tree
Hide file tree
Showing 44 changed files with 505 additions and 783 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ describe('DescopeAccessKeyManagementComponent', () => {
let component: AccessKeyManagementComponent;
let fixture: ComponentFixture<AccessKeyManagementComponent>;
let mockedCreateSdk: jest.Mock;
const onSessionTokenChangeSpy = jest.fn();
const onAccessKeyChangeSpy = jest.fn();
const afterRequestHooksSpy = jest.fn();
const mockConfig: DescopeAuthConfig = {
Expand All @@ -29,7 +28,6 @@ describe('DescopeAccessKeyManagementComponent', () => {
mockedCreateSdk = mocked(createSdk);

mockedCreateSdk.mockReturnValue({
onSessionTokenChange: onSessionTokenChangeSpy,
onAccessKeyChange: onAccessKeyChangeSpy,
httpClient: {
hooks: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ describe('DescopeApplicationsPortalComponent', () => {
let component: ApplicationsPortalComponent;
let fixture: ComponentFixture<ApplicationsPortalComponent>;
let mockedCreateSdk: jest.Mock;
const onSessionTokenChangeSpy = jest.fn();
const onAuditChangeSpy = jest.fn();
const afterRequestHooksSpy = jest.fn();
const mockConfig: DescopeAuthConfig = {
projectId: 'someProject'
Expand All @@ -29,8 +27,6 @@ describe('DescopeApplicationsPortalComponent', () => {
mockedCreateSdk = mocked(createSdk);

mockedCreateSdk.mockReturnValue({
onSessionTokenChange: onSessionTokenChangeSpy,
onAuditChange: onAuditChangeSpy,
httpClient: {
hooks: {
afterRequest: afterRequestHooksSpy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ describe('DescopeAuditManagementComponent', () => {
let component: AuditManagementComponent;
let fixture: ComponentFixture<AuditManagementComponent>;
let mockedCreateSdk: jest.Mock;
const onSessionTokenChangeSpy = jest.fn();
const onAuditChangeSpy = jest.fn();
const afterRequestHooksSpy = jest.fn();
const mockConfig: DescopeAuthConfig = {
projectId: 'someProject'
Expand All @@ -29,8 +27,6 @@ describe('DescopeAuditManagementComponent', () => {
mockedCreateSdk = mocked(createSdk);

mockedCreateSdk.mockReturnValue({
onSessionTokenChange: onSessionTokenChangeSpy,
onAuditChange: onAuditChangeSpy,
httpClient: {
hooks: {
afterRequest: afterRequestHooksSpy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ describe('DescopeComponent', () => {
let fixture: ComponentFixture<DescopeComponent>;
let mockedCreateSdk: jest.Mock;
const onSessionTokenChangeSpy = jest.fn();
const onIsAuthenticatedChangeSpy = jest.fn();
const onUserChangeSpy = jest.fn();
const afterRequestHooksSpy = jest.fn();
const mockConfig: DescopeAuthConfig = {
Expand All @@ -31,6 +32,7 @@ describe('DescopeComponent', () => {

mockedCreateSdk.mockReturnValue({
onSessionTokenChange: onSessionTokenChangeSpy,
onIsAuthenticatedChange: onIsAuthenticatedChangeSpy,
onUserChange: onUserChangeSpy,
httpClient: {
hooks: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ describe('DescopeRoleManagementComponent', () => {
let component: RoleManagementComponent;
let fixture: ComponentFixture<RoleManagementComponent>;
let mockedCreateSdk: jest.Mock;
const onSessionTokenChangeSpy = jest.fn();
const onRoleChangeSpy = jest.fn();
const afterRequestHooksSpy = jest.fn();
const mockConfig: DescopeAuthConfig = {
Expand All @@ -29,7 +28,6 @@ describe('DescopeRoleManagementComponent', () => {
mockedCreateSdk = mocked(createSdk);

mockedCreateSdk.mockReturnValue({
onSessionTokenChange: onSessionTokenChangeSpy,
onRoleChange: onRoleChangeSpy,
httpClient: {
hooks: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ describe('SignInFlowComponent', () => {

mockedCreateSdk.mockReturnValue({
onSessionTokenChange: jest.fn(),
onIsAuthenticatedChange: jest.fn(),
onUserChange: jest.fn()
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ describe('SignUpFlowComponent', () => {

mockedCreateSdk.mockReturnValue({
onSessionTokenChange: jest.fn(),
onIsAuthenticatedChange: jest.fn(),
onUserChange: jest.fn()
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ describe('SignUpOrInFlowComponent', () => {

mockedCreateSdk.mockReturnValue({
onSessionTokenChange: jest.fn(),
onIsAuthenticatedChange: jest.fn(),
onUserChange: jest.fn()
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ describe('DescopeUserManagementComponent', () => {
let component: UserManagementComponent;
let fixture: ComponentFixture<UserManagementComponent>;
let mockedCreateSdk: jest.Mock;
const onSessionTokenChangeSpy = jest.fn();
const onUserChangeSpy = jest.fn();
const afterRequestHooksSpy = jest.fn();
const mockConfig: DescopeAuthConfig = {
Expand All @@ -29,7 +28,6 @@ describe('DescopeUserManagementComponent', () => {
mockedCreateSdk = mocked(createSdk);

mockedCreateSdk.mockReturnValue({
onSessionTokenChange: onSessionTokenChangeSpy,
onUserChange: onUserChangeSpy,
httpClient: {
hooks: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ describe('DescopeUserProfileComponent', () => {
let component: UserProfileComponent;
let fixture: ComponentFixture<UserProfileComponent>;
let mockedCreateSdk: jest.Mock;
const onSessionTokenChangeSpy = jest.fn();
const onAuditChangeSpy = jest.fn();
const afterRequestHooksSpy = jest.fn();
const mockConfig: DescopeAuthConfig = {
projectId: 'someProject'
Expand All @@ -29,8 +27,6 @@ describe('DescopeUserProfileComponent', () => {
mockedCreateSdk = mocked(createSdk);

mockedCreateSdk.mockReturnValue({
onSessionTokenChange: onSessionTokenChangeSpy,
onAuditChange: onAuditChangeSpy,
httpClient: {
hooks: {
afterRequest: afterRequestHooksSpy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ describe('DescopeAuthService', () => {
let mockedCreateSdk: jest.Mock;
let windowSpy: jest.SpyInstance;
const onSessionTokenChangeSpy = jest.fn();
const onIsAuthenticatedChangeSpy = jest.fn();
const onUserChangeSpy = jest.fn();
const getSessionTokenSpy = jest.fn();
const getRefreshTokenSpy = jest.fn();
Expand All @@ -31,6 +32,7 @@ describe('DescopeAuthService', () => {

mockedCreateSdk.mockReturnValue({
onSessionTokenChange: onSessionTokenChangeSpy,
onIsAuthenticatedChange: onIsAuthenticatedChangeSpy,
onUserChange: onUserChangeSpy,
getSessionToken: getSessionTokenSpy,
getRefreshToken: getRefreshTokenSpy,
Expand All @@ -42,6 +44,7 @@ describe('DescopeAuthService', () => {
});

onSessionTokenChangeSpy.mockImplementation((fn) => fn());
onIsAuthenticatedChangeSpy.mockImplementation((fn) => fn());
onUserChangeSpy.mockImplementation((fn) => fn());

TestBed.configureTestingModule({
Expand All @@ -67,6 +70,7 @@ describe('DescopeAuthService', () => {
expect.objectContaining(mockConfig)
);
expect(onSessionTokenChangeSpy).toHaveBeenCalled();
expect(onIsAuthenticatedChangeSpy).toHaveBeenCalled();
expect(onUserChangeSpy).toHaveBeenCalled();
});

Expand Down Expand Up @@ -201,29 +205,22 @@ describe('DescopeAuthService', () => {
describe('refreshSession', () => {
it('correctly handle descopeSession stream when session is successfully refreshed', (done: jest.DoneCallback) => {
refreshSpy.mockReturnValueOnce(
of({ ok: true, data: { sessionJwt: 'newToken' } })
of({
ok: true,
data: { sessionJwt: 'newToken', sessionExpiration: 1663190468 }
})
);
// Taking 4 values from stream: first is initial value, next 3 are the result of refreshSession
service.session$.pipe(take(4), toArray()).subscribe({
// Taking 3 values from stream: first is initial value, next 2 are the result of refreshSession
service.session$.pipe(take(3), toArray()).subscribe({
next: (result) => {
expect(result.slice(1)).toStrictEqual([
{
isAuthenticated: false,
isSessionLoading: true,
sessionToken: undefined
},
{
isAuthenticated: true,
isSessionLoading: true,
sessionToken: 'newToken'
},
{
isAuthenticated: true,
isSessionLoading: false,
sessionToken: 'newToken'
}
expect.objectContaining({
isSessionLoading: true
}),
expect.objectContaining({
isSessionLoading: false
})
]);
expect(service.isAuthenticated()).toBeTruthy();
done();
},
error: (err) => {
Expand All @@ -237,27 +234,17 @@ describe('DescopeAuthService', () => {
refreshSpy.mockReturnValueOnce(
of({ ok: false, data: { sessionJwt: 'newToken' } })
);
// Taking 4 values from stream: first is initial value, next 3 are the result of refreshSession
service.session$.pipe(take(4), toArray()).subscribe({
// Taking 3 values from stream: first is initial value, next 2 are the result of refreshSession
service.session$.pipe(take(3), toArray()).subscribe({
next: (result) => {
expect(result.slice(1)).toStrictEqual([
{
isAuthenticated: false,
isSessionLoading: true,
sessionToken: undefined
},
{
isAuthenticated: false,
isSessionLoading: true,
sessionToken: ''
},
{
isAuthenticated: false,
isSessionLoading: false,
sessionToken: ''
}
expect.objectContaining({
isSessionLoading: true
}),
expect.objectContaining({
isSessionLoading: false
})
]);
expect(service.isAuthenticated()).toBeFalsy();
done();
},
error: (err) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export class DescopeAuthService {
});
this.user$ = this.userSubject.asObservable();
this.descopeSdk.onSessionTokenChange(this.setSession.bind(this));
this.descopeSdk.onIsAuthenticatedChange(this.setIsAuthenticated.bind(this));
this.descopeSdk.onUserChange(this.setUser.bind(this));
}

Expand All @@ -63,22 +64,6 @@ export class DescopeAuthService {
isSessionLoading: true
});
return this.descopeSdk.refresh().pipe(
tap((data) => {
const afterRequestSession = this.sessionSubject.value;
if (data.ok && data.data) {
this.sessionSubject.next({
...afterRequestSession,
sessionToken: data.data.sessionJwt,
isAuthenticated: !!data.data.sessionJwt
});
} else {
this.sessionSubject.next({
...afterRequestSession,
sessionToken: '',
isAuthenticated: false
});
}
}),
finalize(() => {
const afterRefreshSession = this.sessionSubject.value;
this.sessionSubject.next({
Expand Down Expand Up @@ -185,9 +170,16 @@ export class DescopeAuthService {
private setSession(sessionToken: string | null) {
const currentSession = this.sessionSubject.value;
this.sessionSubject.next({
sessionToken,
isAuthenticated: !!sessionToken,
isSessionLoading: currentSession.isSessionLoading
...currentSession,
sessionToken
});
}

private setIsAuthenticated(isAuthenticated: boolean) {
const currentSession = this.sessionSubject.value;
this.sessionSubject.next({
...currentSession,
isAuthenticated
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ describe('DescopeInterceptor', () => {
mockedCreateSdk = mocked(createSdk);
mockedCreateSdk.mockReturnValue({
onSessionTokenChange: jest.fn(),
onIsAuthenticatedChange: jest.fn(),
onUserChange: jest.fn()
});

Expand Down Expand Up @@ -72,7 +73,12 @@ describe('DescopeInterceptor', () => {
jest.spyOn(authService, 'getSessionToken').mockReturnValue(null);
const refreshSessionSpy = jest
.spyOn(authService, 'refreshSession')
.mockReturnValue(of({ ok: true, data: { sessionJwt: 'newToken' } }));
.mockReturnValue(
of({
ok: true,
data: { sessionJwt: 'newToken', sessionExpiration: 1663190468 }
})
);

httpClient.get('/api/data').subscribe();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ jest.mock('@descope/web-js-sdk');
describe('AppComponent', () => {
let mockedCreateSdk: jest.Mock;
const onSessionTokenChangeSpy = jest.fn();
const onIsAuthenticatedChangeSpy = jest.fn();
const onUserChangeSpy = jest.fn();

beforeEach(async () => {
mockedCreateSdk = mocked(createSdk);
mockedCreateSdk.mockReturnValue({
onSessionTokenChange: onSessionTokenChangeSpy,
onIsAuthenticatedChange: onIsAuthenticatedChangeSpy,
onUserChange: onUserChangeSpy
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ describe('HomeComponent', () => {

let mockedCreateSdk: jest.Mock;
const onSessionTokenChangeSpy = jest.fn();
const onIsAuthenticatedChangeSpy = jest.fn();
const onUserChangeSpy = jest.fn();

beforeEach(async () => {
mockedCreateSdk = mocked(createSdk);
mockedCreateSdk.mockReturnValue({
onSessionTokenChange: onSessionTokenChangeSpy,
onIsAuthenticatedChange: onIsAuthenticatedChangeSpy,
onUserChange: onUserChangeSpy
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ describe('LoginComponent', () => {

let mockedCreateSdk: jest.Mock;
const onSessionTokenChangeSpy = jest.fn();
const onIsAuthenticatedChangeSpy = jest.fn();
const onUserChangeSpy = jest.fn();

beforeEach(async () => {
mockedCreateSdk = mocked(createSdk);
mockedCreateSdk.mockReturnValue({
onSessionTokenChange: onSessionTokenChangeSpy,
onIsAuthenticatedChange: onIsAuthenticatedChangeSpy,
onUserChange: onUserChangeSpy
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ describe('ProtectedComponent', () => {

let mockedCreateSdk: jest.Mock;
const onSessionTokenChangeSpy = jest.fn();
const onIsAuthenticatedChangeSpy = jest.fn();
const onUserChangeSpy = jest.fn();

beforeEach(async () => {
mockedCreateSdk = mocked(createSdk);
mockedCreateSdk.mockReturnValue({
onSessionTokenChange: onSessionTokenChangeSpy,
onIsAuthenticatedChange: onIsAuthenticatedChangeSpy,
onUserChange: onUserChangeSpy
});

Expand Down
1 change: 1 addition & 0 deletions packages/sdks/core-js-sdk/src/sdk/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export type JWTResponse = {
cookieExpiration?: number;
user?: UserResponse;
firstSeen?: boolean;
sessionExpiration: number;
};

/** Authentication info result from exchanging access keys for a session */
Expand Down
Loading

0 comments on commit 0cd7ee3

Please sign in to comment.