Skip to content

Commit

Permalink
Implemented logout flow by saving nameId/sessionIndex in token payload
Browse files Browse the repository at this point in the history
Also added ability to provide additional config options for things such as relay_state.

Closes #1
  • Loading branch information
marcusforsberg committed Feb 12, 2020
1 parent a892ed5 commit 97f742e
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 44 deletions.
37 changes: 24 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,19 @@ Configuration and usage will depend on your IdP. To get started, configure your
"sso_login_url": "https://some-idp.org/saml/SSO",
"sso_logout_url": "https://some-idp.org/saml/SLO",
"certificates": ["idp-cert.crt"]
}
},
"loginRequestOptions": {
// Optional additional options for the login request, such as "relay_state", see https://www.npmjs.com/package/saml2-js#create_login_request_url
},
"loginResponseOptions": {
// Optional additional options for the login response, such as "relay_state", see https://www.npmjs.com/package/saml2-js#post_assert
},
"logoutRequestOptions": {
// Optional additional options for the logout request, such as "relay_state", see https://www.npmjs.com/package/saml2-js#create_logout_response_url
},
"logoutResponseOptions": {
// Optional additional options for the logout response, such as "relay_state", see https://www.npmjs.com/package/saml2-js#create_logout_request_url
},
}
}
}
Expand All @@ -71,10 +83,10 @@ In addition to the `saml2-js` options, the following settings are available:
- The [SamlStrategy](#samlstrategy) is invoked, which
- Gets the users profile
- Finds or creates the user (entity) for that profile
- The [AuthenticationService](https://docs.feathersjs.com/api/authentication/service.html) creates an access token for that entity
- The [AuthenticationService](https://docs.feathersjs.com/api/authentication/service.html) creates an access token for that entity. The token payload will additionally include `nameId` and `sessionIndex` which are required later for logout.
- Redirect to the `redirect` URL including the generated access token
- The frontend (e.g. [authentication client](https://docs.feathersjs.com/api/authentication/client.html)) uses the returned access token to authenticate
- The frontend can redirect the user to `/saml/logout` to trigger the SAML logout flow (Note: not yet functional)
- The frontend can redirect the user to `/saml/logout?nameId=xxx&sessionIndex=xxx` to trigger the SAML logout flow, providing the `nameId` and `sessionIndex` from the access token payload

### SAML URLs

Expand All @@ -83,14 +95,21 @@ There are several URLs and redirects that are important for SAML authentication:
- `http(s)://<host>/saml`: The main URL to initiate the SAML flow. Link to this from the browser.
- `http(s)://<host>/saml/metadata.xml`: The URL to the generated SP metadata file, to be provided to the IdP.
- `http(s)://<host>/saml/assert`: The ACS that the IdP will redirect back to for validation.
- `http(s)://<host>/saml/logout`: The URL to trigger the SAML logout flow (Note: not yet functional)
- `http(s)://<host>/saml/logout`: The URL to trigger the SAML logout flow
- `http(s)://<host>/saml/sso`: The SLO endpoint the IdP should redirect to after a successful logout

In the browser a SAML flow can be initiated with a link like:

```html
<a href="/saml">Login with IdentityProvider</a>
```

or

```html
<a href="/saml/logout?nameId={{ nameIdFromJWTPayload }}&sessionIndex={{ sessionIndexFromJWTPayload }}">Logout</a>
```

### Redirects

> __Note:__ This functionality is stolen directly from [@feathersjs/authentication-oauth](https://github.com/feathersjs/feathers/tree/master/packages/authentication-oauth)
Expand Down Expand Up @@ -152,10 +171,7 @@ class MySamlStrategy extends SamlStrategy {
async getEntityData (samlUser: SamlUser, _existingEntity: any, params) {
return {
[`email`]: samlUser.attributes.email,
[`fullName`]: samlUser.attributes.fullName,
// NameId and SessionIndex are required if you wish to use SAML Logout as well. This sample stores them on the user in your DB.
[`nameId`]: samlUser.name_id,
[`sessionIndex`]: samlUser.session_index
[`fullName`]: samlUser.attributes.fullName
};
}
}
Expand Down Expand Up @@ -209,11 +225,6 @@ class MySamlStrategy extends SamlStrategy {

`samlStrategy.authenticate(authentication, params)` is the main endpoint implemented by any [authentication strategy](https://docs.feathersjs.com/api/authentication/strategy.html). It is usually called for authentication requests for this strategy by the [AuthenticationService](https://docs.feathersjs.com/api/authentication/service.html).

## TODO

- SAML Logout is currently not functional.
- Write tests

## License

Heavily based on [@feathersjs/authentication-oauth](https://github.com/feathersjs/feathers/tree/master/packages/authentication-oauth). All credit to daffl and the other amazing Feathers contributors for all their hard work. :)
Expand Down
96 changes: 65 additions & 31 deletions src/express.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from '@feathersjs/express';
import { SamlSetupSettings } from './utils';
import { SamlStrategy } from './strategy';
import { BadRequest } from '@feathersjs/errors';

const debug = Debug('feathers-saml/express');

Expand Down Expand Up @@ -37,24 +38,24 @@ export default (options: SamlSetupSettings) => {
const authApp = express();

authApp.get('/', async (req, res) => {
sp.create_login_request_url(idp, {}, async (err: Error, login_url: string, request_id: string) => {
if (err != null) {
return res.send(500);
}
sp.create_login_request_url(idp, config.loginRequestOptions ? config.loginRequestOptions : {}, async (err: Error, login_url: string, request_id: string) => {
if (err != null) {
return res.send(500);
}

res.redirect(login_url);
});
res.redirect(login_url);
});
});

authApp.get('/metadata.xml', async (req, res) => {
res.type('application/xml');
res.send(sp.create_metadata());
res.type('application/xml');
res.send(sp.create_metadata());
});

authApp.post('/assert', async (req, res, next) => {
const service = app.defaultAuthentication(authService);
const [ strategy ] = service.getStrategies('saml') as SamlStrategy[];
const params = {
const params: any = {
authStrategies: [ strategy.name ]
};
const sendResponse = async (data: AuthenticationResult|Error) => {
Expand All @@ -76,24 +77,34 @@ export default (options: SamlSetupSettings) => {

try {
const samlResponse: any = await new Promise((resolve, reject) => {
sp.post_assert(idp, {
request_body: req.body
},
async (err: Error, saml_response: any) => {
if (err != null) {
reject(err);
return;
}

resolve(saml_response);
});
let loginResponseOptions: any = {};

if (config.loginResponseOptions) {
loginResponseOptions = config.loginResponseOptions;
}

loginResponseOptions.request_body = req.body;

sp.post_assert(idp, loginResponseOptions, async (err: Error, saml_response: any) => {
if (err != null) {
reject(err);
return;
}

resolve(saml_response);
});
});

const authentication = {
strategy: strategy.name,
...samlResponse
};

params.payload = {
nameId: samlResponse && samlResponse.user && samlResponse.user.name_id ? samlResponse.user.name_id : null,
sessionIndex: samlResponse && samlResponse.user && samlResponse.user.session_index ? samlResponse.user.session_index : null
};

debug(`Calling ${authService}.create authentication with SAML strategy`);

const authResult = await service.create(authentication, params);
Expand All @@ -107,21 +118,44 @@ export default (options: SamlSetupSettings) => {
}
});

// TODO: Implement logout. Need a way to get NameId and SessionIndex here.
authApp.get('/logout', async (req, res, next) => {
sp.create_logout_request_url(idp, {
name_id: null,
session_index: null
}, async (err: Error, logout_url: string) => {
if (err != null) {
next(err);
return;
}
const { nameId, sessionIndex } = req.query;

res.redirect(logout_url);
});
if (!nameId || !sessionIndex) {
return next(new BadRequest('`nameId` and `sessionIndex` must be set in query params'));
}

let logoutRequestOptions: any = {};

if (config.logoutRequestOptions) {
logoutRequestOptions = config.logoutRequestOptions;
}

logoutRequestOptions.name_id = nameId;
logoutRequestOptions.session_ndex = sessionIndex;

sp.create_logout_request_url(idp, logoutRequestOptions, async (err: Error, logout_url: string) => {
if (err != null) {
next(err);
return;
}

res.redirect(logout_url);
});
});

authApp.get('/slo', async (req, res, next) => {
sp.create_logout_response_url(idp, config.logoutResponseOptions ? config.logoutResponseOptions : {}, async (err: Error, request_url: string) => {
if (err != null) {
next(err);
return;
}

res.redirect(request_url);
});
});


app.use(path, authApp);
};
};

0 comments on commit 97f742e

Please sign in to comment.