Skip to content

Commit

Permalink
Merge pull request #13 from atifcppprogrammer/feature/post-login-redi…
Browse files Browse the repository at this point in the history
…rect

Implement post login redirect like `@kinde-oss/kinde-auth-nextjs`
  • Loading branch information
DanielRivers authored Feb 14, 2024
2 parents 9f99f2a + a45c9a1 commit f9f2c6c
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 3 deletions.
51 changes: 48 additions & 3 deletions my_custom_templates/KindeClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { SDK_VERSION } from "./sdk/utils/SDKVersion";
* @property {String} options.clientSecret - Client secret of the application
* @property {String} options.redirectUri - Redirection URI registered in the authorization server
* @property {String} options.logoutRedirectUri - URI to redirect the user after logout
* @property {String} options.postLoginRedirectUri - URI to redirect the user after login
* @property {String} options.grantType - Grant type for the authentication process (client_credentials, authorization_code or pkce)
* @property {String} options.audience - API Identifier for the target API (Optional)
* @property {String} options.scope - List of scopes requested by the application (default: 'openid profile email offline')
Expand All @@ -31,6 +32,7 @@ export default class KindeClient extends ApiClient {
clientSecret,
redirectUri,
logoutRedirectUri,
postLoginRedirectUri = '',
grantType,
audience = '',
scope = 'openid profile email offline',
Expand Down Expand Up @@ -73,6 +75,11 @@ export default class KindeClient extends ApiClient {
}
this.logoutRedirectUri = logoutRedirectUri;

if (postLoginRedirectUri && typeof postLoginRedirectUri !== 'string') {
throw new Error('Provided postLoginRedirectUri must be a string');
}
this.postLoginRedirectUri = postLoginRedirectUri

this.audience = audience;
this.scope = scope;
this.kindeSdkLanguage = kindeSdkLanguage;
Expand All @@ -90,13 +97,15 @@ export default class KindeClient extends ApiClient {
* @property {Object} request - The HTTP request object
* @property {String} request.query.state - Optional parameter used to pass a value to the authorization server
* @property {String} request.query.org_code - Organization code
* @property {String} request.query.post_login_redirect_url - URL to redirect the user after login
*/
login() {
return async (req, res, next) => {
const sessionId = getSessionId(req);
const {
state = randomString(),
org_code,
post_login_redirect_url = '',
} = req.query;

if (SessionStore.getDataByKey(sessionId, 'kindeAccessToken') && !this.isTokenExpired(sessionId)) {
Expand All @@ -123,6 +132,9 @@ export default class KindeClient extends ApiClient {
org_code,
start_page: 'login',
});
if (post_login_redirect_url) {
SessionStore.setDataByKey(sessionId, 'kindePostLoginRedirectUrl', post_login_redirect_url);
}
SessionStore.setDataByKey(sessionId, 'kindeOauthState', state);
res.cookie('kindeSessionId', sessionId, CookieOptions);
return res.redirect(authorizationURL);
Expand All @@ -136,6 +148,9 @@ export default class KindeClient extends ApiClient {
org_code,
start_page: 'login',
}, codeChallenge);
if (post_login_redirect_url) {
SessionStore.setDataByKey(sessionId, 'kindePostLoginRedirectUrl', post_login_redirect_url);
}
SessionStore.setDataByKey(sessionId, 'kindeOauthState', state);
SessionStore.setDataByKey(sessionId, 'kindeOauthCodeVerifier', codeVerifier);
res.cookie('kindeSessionId', sessionId, CookieOptions);
Expand All @@ -153,13 +168,15 @@ export default class KindeClient extends ApiClient {
* @property {Object} request - The HTTP request object
* @property {String} request.query.state - Optional parameter used to pass a value to the authorization server
* @property {String} request.query.org_code - Organization code
* @property {String} request.query.post_login_redirect_url - URL to redirect the user after login
*/
register() {
return (req, res, next) => {
const sessionId = getSessionId(req);
const {
state = randomString(),
org_code,
post_login_redirect_url = '',
} = req.query;

if (SessionStore.getDataByKey(sessionId, 'kindeAccessToken') && !this.isTokenExpired(sessionId)) {
Expand All @@ -176,6 +193,9 @@ export default class KindeClient extends ApiClient {
org_code,
start_page: 'registration',
});
if (post_login_redirect_url) {
SessionStore.setDataByKey(sessionId, 'kindePostLoginRedirectUrl', post_login_redirect_url);
}
SessionStore.setDataByKey(sessionId, 'kindeOauthState', state);
res.cookie('kindeSessionId', sessionId, CookieOptions);
return res.redirect(authorizationURL);
Expand All @@ -189,6 +209,9 @@ export default class KindeClient extends ApiClient {
org_code,
start_page: 'registration',
}, codeChallenge);
if (post_login_redirect_url) {
SessionStore.setDataByKey(sessionId, 'kindePostLoginRedirectUrl', post_login_redirect_url);
}
SessionStore.setDataByKey(sessionId, 'kindeOauthState', state);
SessionStore.setDataByKey(sessionId, 'kindeOauthCodeVerifier', codeVerifier);
res.cookie('kindeSessionId', sessionId, CookieOptions);
Expand Down Expand Up @@ -237,17 +260,28 @@ export default class KindeClient extends ApiClient {

// Determine the grant type and get the access token
switch (this.grantType) {
case GrantType.AUTHORIZATION_CODE:
case GrantType.AUTHORIZATION_CODE: {
auth = new AuthorizationCode();
res_get_token = await auth.getToken(this, code);
if (res_get_token?.error) {
const msg = res_get_token?.error_description || res_get_token?.error;
return next(new Error(msg));
}
const postLoginRedirectUrlFromStore = SessionStore.getDataByKey(sessionId, 'kindePostLoginRedirectUrl')
if (postLoginRedirectUrlFromStore) {
SessionStore.removeDataByKey(sessionId, 'kindePostLoginRedirectUrl');
}
this.saveToken(sessionId, res_get_token);
const postLoginRedirectUrl = postLoginRedirectUrlFromStore
? postLoginRedirectUrlFromStore
: this.postLoginRedirectUri;
if (postLoginRedirectUrl) {
return res.redirect(postLoginRedirectUrl);
}
return next();
}

case GrantType.PKCE:
case GrantType.PKCE: {
const codeVerifier = SessionStore.getDataByKey(sessionId, 'kindeOauthCodeVerifier');
if (!codeVerifier) {
return next(new Error('Not found code_verifier'));
Expand All @@ -258,8 +292,19 @@ export default class KindeClient extends ApiClient {
const msg = res_get_token?.error_description || res_get_token?.error;
return next(new Error(msg));
}
const postLoginRedirectUrlFromStore = SessionStore.getDataByKey(sessionId, 'kindePostLoginRedirectUrl')
if (postLoginRedirectUrlFromStore) {
SessionStore.removeDataByKey(sessionId, 'kindePostLoginRedirectUrl');
}
this.saveToken(sessionId, res_get_token);
const postLoginRedirectUrl = postLoginRedirectUrlFromStore
? postLoginRedirectUrlFromStore
: this.postLoginRedirectUri;
if (postLoginRedirectUrl) {
return res.redirect(postLoginRedirectUrl);
}
return next();
}
}
} catch (err) {
this.clearSession(sessionId, res);
Expand Down Expand Up @@ -542,7 +587,7 @@ export default class KindeClient extends ApiClient {
clearSession(sessionId, response) {
SessionStore.removeData(sessionId);
delete this.authentications.kindeBearerAuth.accessToken;
response.clearCookie('sessionId');
response.clearCookie('kindeSessionId');
}

/**
Expand Down
83 changes: 83 additions & 0 deletions my_custom_templates/KindeClient.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ import sinon from 'sinon';
expect(KindeManagementApi.SessionStore.getDataByKey('session-id', 'kindeOauthState')).to.be('random_state');
});

it('stores provided "post_login_redirect_url" query param to memory', async () => {
const sessionId = 'session-id';
req.query = { post_login_redirect_url: '/post-login-url' };
req.headers.cookie = `kindeSessionId=${sessionId}`;
KindeManagementApi.SessionStore.setData(sessionId, {});
await instance.login()(req, res, next);
const cachedPostLoginParam = KindeManagementApi.SessionStore.getDataByKey(sessionId, 'kindePostLoginRedirectUrl');
expect(cachedPostLoginParam).to.be(req.query.post_login_redirect_url);
});

it('should call login throw an error', async () => {
req.query = { state: 'random_state', org_code: 'org-code' };
await instance.login()(req, {}, next);
Expand Down Expand Up @@ -126,6 +136,16 @@ import sinon from 'sinon';
expect(KindeManagementApi.SessionStore.getDataByKey('session-id', 'kindeOauthState')).to.be('random_state');
});

it('stores provided "post_login_redirect_url" query param to memory', async () => {
const sessionId = 'session-id';
req.query = { post_login_redirect_url: '/post-login-url' };
req.headers.cookie = `kindeSessionId=${sessionId}`;
KindeManagementApi.SessionStore.setData(sessionId, {});
await instance.login()(req, res, next);
const cachedPostLoginParam = KindeManagementApi.SessionStore.getDataByKey(sessionId, 'kindePostLoginRedirectUrl');
expect(cachedPostLoginParam).to.be(req.query.post_login_redirect_url);
});

it('should call register throw an error', async () => {
await instance.register()(req, {}, next);
expect(next.calledOnce).to.be(true);
Expand Down Expand Up @@ -157,6 +177,7 @@ import sinon from 'sinon';
};
const base64Payload = Buffer.from(JSON.stringify(payloadAccessToken)).toString('base64');
const validToken = `header.${base64Payload}.signature`;
const sessionId = 'session-id';
beforeEach(() => {
req = {
headers: {
Expand All @@ -173,6 +194,7 @@ import sinon from 'sinon';
});

afterEach(() => {
KindeManagementApi.SessionStore.removeData(sessionId);
sandbox.restore();
});

Expand All @@ -197,6 +219,67 @@ import sinon from 'sinon';
saveTokenStub.restore();
});

it('redirects to cached "kindePostLoginRedirectUrl" when one exists', async () => {
const cachedPostLoginParam = '/post-login-url';
req.query = { state: 'random_state', code: 'code' };
req.headers.cookie = `kindeSessionId=${sessionId}`;
KindeManagementApi.SessionStore.setData(sessionId, {
kindeOauthState: 'random_state',
kindePostLoginRedirectUrl: cachedPostLoginParam,
});
const getTokenStub = sinon.stub(KindeManagementApi.AuthorizationCode.prototype, 'getToken').resolves({
access_token: validToken,
id_token: validToken,
expires_in: 86399,
refresh_token: 'my.refresh-token.example',
});

await instance.callback()(req, res, next);
expect(res.redirect.calledOnce).to.be(true);
expect(res.redirect.firstCall.args[0]).to.be(cachedPostLoginParam);
expect(KindeManagementApi.SessionStore.getDataByKey(sessionId, 'kindePostLoginRedirectUrl')).to.be(undefined);
getTokenStub.restore();
});

it('redirects to "this.postLoginRedirectUri" when "kindePostLoginRedirectUrl" is not cached but env-variable exists', async () => {
req.query = { state: 'random_state', code: 'code' };
req.headers.cookie = `kindeSessionId=${sessionId}`;
KindeManagementApi.SessionStore.setData(sessionId, { kindeOauthState: 'random_state' });
const getTokenStub = sinon.stub(KindeManagementApi.AuthorizationCode.prototype, 'getToken').resolves({
access_token: validToken,
id_token: validToken,
expires_in: 86399,
refresh_token: 'my.refresh-token.example',
});
const removeDataByKeyStub = sinon.stub(KindeManagementApi.SessionStore, 'removeDataByKey');

const postLoginRedirectUri = '/post-login-url';
const clientOptions = {...options, postLoginRedirectUri, };
const newInstance = new KindeManagementApi.KindeClient(clientOptions);
await newInstance.callback()(req, res, next);
expect(res.redirect.calledOnce).to.be(true);
expect(removeDataByKeyStub.notCalled).to.be(true);
expect(res.redirect.firstCall.args[0]).to.be(postLoginRedirectUri);
removeDataByKeyStub.restore();
getTokenStub.restore();
});

it('no post login redirect if neither "kindePostLoginRedirectUrl" is cached or env-variable exists', async () => {
req.query = { state: 'random_state', code: 'code' };
req.headers.cookie = `kindeSessionId=${sessionId}`;
KindeManagementApi.SessionStore.setData(sessionId, { kindeOauthState: 'random_state' });
const getTokenStub = sinon.stub(KindeManagementApi.AuthorizationCode.prototype, 'getToken').resolves({
access_token: validToken,
id_token: validToken,
expires_in: 86399,
refresh_token: 'my.refresh-token.example',
});
await instance.callback()(req, res, next);
expect(res.redirect.notCalled).to.be(true);
expect(next.calledOnce).to.be(true);
getTokenStub.restore();
});

it('should call callback throws an error', async () => {
await instance.callback()(req, {}, next);
expect(next.calledOnce).to.be(true);
Expand Down

0 comments on commit f9f2c6c

Please sign in to comment.