Skip to content

Commit ea997ab

Browse files
authored
Development (#159)
* add state validation for authorization * fix codacy errors * update state parameter storage * fix codacy errors * remove random number test * encode state param to base64 * token manager for custom identity * Added mock tests for token manager * Added tests for token manager config * Added sample node app for custom identity * Added custom identity to README file * minor fix * Made changes to config tests * Minor change * Made changes from Code Review * Fixed state parameter validation and bug in token validation * Fixed bug in validate token * Fixed spelling in README * Fixed spacing * Minor fix * Refactoring * Made change from code review * Bug fix * update return of error from callback * update callback error * Bypass State validation for cloud directory update req (#121) * bypass state validation for cloud directory update req * update flow * update encoding for state param to base64URL encode * Removed tenant Id from API strategy and WebAppStrategy (#120) * Removed tenant Id validation for API strategy * Removed tenantId as requirement for initializing APIStrategy * Updated README file * Fixed spacing issue from code review * Removed tenant Id validation from WebAPIStrategy * Removed tenant Id validation from Custom Identity * Updated code samples with changes for tenant Id validation * Bump up to 4.1.1 (#123) * Application identity (#125) * Adds support for application identity (app to app flow) * Adds test cases for application identity * Adds documentation on how to use App to App flow * fixes spacing * Changes from PR * Bump up version number to 4.2 * Correct version to be correct format * Renaming to application Identity and authorization (#127) * Update README.md * Update package.json * call logging api from node SDK (#130) * call logging api from node SDK * call logging api from node SDK * refactor * do not log the legacy sample logout * do not log the legacy sample logout * add debug message * a more generic error message * fix error message * Updated the versions of dependencies * Add initialization examples for TokenManager (#133) https://github.ibm.com/security-services/appid-project-management/issues/2043 * Adding support for the new service endpoint, this endpoint supposed to work in parallel with the existing oauth server endpoints Add eslint to our code that should validate coding conventions * fix version parameter (#138) * fix version * Update tests * Update tests * Multi tenants (#143) * add multi-tenant support through adding a publicKeysJson object * update test error message * remove get and set public key endpoint, add clarity to code, change var to let * update and fix tests * change isUpdateRequestPending to unique array * move getPublicKeyByKid up * remove unnecessary publicKey * accept both oauthServerUrl and oAuthServerUrl (#147) * accept both oauthServerUrl and oAuthServerUrl * Issue 2287 -- Validation Changes Only (#145) Validation changes -- ISS, AZP, AUD * Issue 2439 (#148) Renamed azp validation function and accommodates v3 and v4 tokens * Issue 2439-2 (#149) minor token change * fixing Application Identity code snippet (#150) * Bump up version to 6.0.0 * fixes tests (#152) * check for LOG4JS_CONFIG variable set and if not present use log4js.json as default config (#154) * Updates default log level to info from debug (#155) * Log4js update (#156) * update to use Log4js.configure instead of global variable * added slack link * Rani access control (#160) * added hasScope method to token-manager * moved hasScope method to token-utils * field renames * added prefix to required scopes in token-util added scope validation to api-strategy when providing scope and audience * added tests for api-strategy's scope validation * added unit testing * changed the hasScope method: it now ignores scope prefixes (takes the part that is after the last slash) * added an optional appUri argument for loadConfig which is used by webapp-strategy and api-strategy's constructors. * moved hasScope method to WebAppStrategy as a static method. cleaning, renaming, added validation for user input. * added tests * added tests * added documentation on using access control in readme * fixed typo in readme * fixed typo in readme * Add better documentation for audience (#164) * fix the token audience claim test (#165) * Update package.json (#167) * update readme * Cleaning app uri (#168) * removed appUri * commented out parts about access control in readme * update to 6.0.2
1 parent 21aab31 commit ea997ab

File tree

9 files changed

+1081
-749
lines changed

9 files changed

+1081
-749
lines changed

README.md

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,24 @@ app.listen(port, function(){
109109
});
110110

111111
```
112-
112+
<!---
113+
##### Protecting APIs using the APIStrategy: Access Control
114+
Using access control, you can define the scopes that are required to access a specific endpoint.
115+
```JavaScript
116+
app.get("/api/protected",
117+
passport.authenticate(APIStrategy.STRATEGY_NAME, {
118+
audience: "myApp",
119+
scope: "read write update"
120+
}),
121+
function(req, res) {
122+
res.send("Hello from protected resource");
123+
}
124+
);
125+
```
126+
The scope parameter defines the required scopes.
127+
The audience parameter is optional and should be set to the application clientId
128+
to guarantee the scopes are for the requested application.
129+
-->
113130
#### Protecting web applications using WebAppStrategy
114131
WebAppStrategy is based on the OAuth2 authorization_code grant flow and should be used for web applications that use browsers. The strategy provides tools to easily implement authentication and authorization flows. When WebAppStrategy provides mechanisms to detect unauthenticated attempts to access protected resources. The WebAppStrategy will automatically redirect user's browser to the authentication page. After successful authentication user will be taken back to the web application's callback URL (redirectUri), which will once again use WebAppStrategy to obtain access, identity and refresh tokens from App ID service. After obtaining these tokens the WebAppStrategy will store them in HTTP session under WebAppStrategy.AUTH_CONTEXT key. In a scalable cloud environment it is recommended to persist HTTP sessions in a scalable storage like Redis to ensure they're available across server app instances.
115132

@@ -207,7 +224,21 @@ app.get("/protected", passport.authenticate(WebAppStrategy.STRATEGY_NAME), funct
207224
// Start the server!
208225
app.listen(process.env.PORT || 1234);
209226
```
210-
227+
<!---
228+
##### Protecting web applications using WebAppStrategy: Access Control
229+
Using access control, you can check which scopes exist on the request.
230+
```JavaScript
231+
app.get("/protected", passport.authenticate(WebAppStrategy.STRATEGY_NAME), function(req, res){
232+
if(WeAppStrategy.hasScope(req, "read write")){
233+
res.json(req.user);
234+
}
235+
else {
236+
res.send("insufficient scopes!");
237+
}
238+
});
239+
```
240+
Use WebAppStrategy's hasScope method to check if a given request has some specific scopes.
241+
-->
211242
#### Anonymous login
212243
WebAppStrategy allows users to login to your web application anonymously, meaning without requiring any credentials. After successful login the anonymous user access token will be persisted in HTTP session, making it available as long as HTTP session is kept alive. Once HTTP session is destroyed or expired the anonymous user access token will be destroyed as well.
213244

@@ -554,6 +585,14 @@ selfServiceManager.updateUserDetails(uuid, userData, iamToken).then(function (us
554585
}
555586
```
556587

588+
### Logging
589+
This SDK uses the log4js package for logging. By default the logging level is set to `info`. To create your own logging configuration for your application, add a log4js.json file and set the `process.env.LOG4JS_CONFIG` environment variable to your json file.
590+
591+
To learn more about log4js, visit the documentation here (https://log4js-node.github.io/log4js-node/).
592+
593+
## Got Questions?
594+
Join us on [Slack](https://www.ibm.com/cloud/blog/announcements/get-help-with-ibm-cloud-app-id-related-questions-on-slack) and chat with our dev team.
595+
557596

558597
### Logging
559598
This SDK uses the log4js package for logging. By default the logging level is set to `info`. To create your own logging configuration for your application, add a log4js.json file and set the `process.env.LOG4JS_CONFIG` environment variable to your json file.

lib/strategies/api-strategy.js

Lines changed: 125 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ const ServiceUtil = require('../utils/service-util');
1818
const PublicKeyUtil = require("../utils/public-key-util");
1919

2020
const ERROR = {
21-
INVALID_REQUEST: "invalid_request", // HTTP 400
22-
INVALID_TOKEN: "invalid_token", // HTTP 401
23-
INSUFFICIENT_SCOPE: "insufficient_scope" // HTTP 401
21+
INVALID_REQUEST: "invalid_request", // HTTP 400
22+
INVALID_TOKEN: "invalid_token", // HTTP 401
23+
INSUFFICIENT_SCOPE: "insufficient_scope" // HTTP 401
2424
};
2525

2626
const AUTHORIZATION_HEADER = "Authorization";
@@ -30,119 +30,145 @@ const BEARER = "Bearer";
3030
const logger = log4js.getLogger(STRATEGY_NAME);
3131

3232
function ApiStrategy(options) {
33-
logger.debug("Initializing");
34-
options = options || {};
35-
this.name = ApiStrategy.STRATEGY_NAME;
36-
this.serviceConfig = new ServiceUtil.loadConfig('APIStrategy', [constants.OAUTH_SERVER_URL], options);
33+
logger.debug("Initializing");
34+
options = options || {};
35+
this.name = ApiStrategy.STRATEGY_NAME;
36+
this.serviceConfig = new ServiceUtil.loadConfig('APIStrategy', [constants.OAUTH_SERVER_URL], options);
3737
}
3838

3939
ApiStrategy.STRATEGY_NAME = STRATEGY_NAME;
4040
ApiStrategy.DEFAULT_SCOPE = "appid_default";
4141

42-
ApiStrategy.prototype.authenticate = function (req, options={}) {
43-
var self = this;
44-
logger.debug("authenticate");
45-
46-
var requiredScope = ApiStrategy.DEFAULT_SCOPE;
47-
if (options.scope) {
48-
requiredScope += " " + options.scope;
42+
/**
43+
*
44+
* @param req - an HTTP request object
45+
* @param options.scope - The required scopes, separated by spaces. For example: 'read write update'
46+
* @param options.audience - (optional) the application clientId, or the resource URI.
47+
* @returns {*}
48+
*/
49+
ApiStrategy.prototype.authenticate = function (req, options = {}) {
50+
var self = this;
51+
logger.debug("authenticate");
52+
53+
if (options.scope && typeof options.scope !== 'string' || options.audience && typeof options.audience !== 'string') {
54+
return self.fail(buildWwwAuthenticateHeader('Illegal Scope', ERROR.INVALID_REQUEST), 400);
55+
}
56+
57+
let requiredScopes = ApiStrategy.DEFAULT_SCOPE;
58+
if (options.scope && options.scope.trim()) { // if the required scopes are just whitespace, skip.
59+
// split by spaces and keep only non empty scopes. excluded default scope as it is always there
60+
const scopesArray = options.scope.split(" ").filter(scope => scope !== '' && scope !== ApiStrategy.DEFAULT_SCOPE);
61+
for (const requiredScope of scopesArray) {
62+
requiredScopes += " " + requiredScope;
4963
}
50-
51-
// Retrieve authorization header from request
52-
var authHeader = req.header(AUTHORIZATION_HEADER);
53-
if (!authHeader) {
54-
logger.warn("Authorization header not found");
55-
return self.fail(buildWwwAuthenticateHeader(requiredScope, ERROR.INVALID_TOKEN), 401);
64+
}
65+
66+
// Retrieve authorization header from request
67+
const authHeader = req.header(AUTHORIZATION_HEADER);
68+
if (!authHeader) {
69+
logger.warn("Authorization header not found");
70+
return self.fail(buildWwwAuthenticateHeader(requiredScopes, ERROR.INVALID_TOKEN), 401);
71+
}
72+
73+
// Validate that first header component is Bearer
74+
const authHeaderComponents = authHeader.split(" ");
75+
if (authHeaderComponents[0].indexOf(BEARER) !== 0) {
76+
logger.warn("Malformed authorization header");
77+
return self.fail(buildWwwAuthenticateHeader(requiredScopes, ERROR.INVALID_TOKEN), 401);
78+
}
79+
80+
// Validate header has exactly 2 or 3 components (Bearer, access_token, [id_token])
81+
if (authHeaderComponents.length !== 2 && authHeaderComponents.length !== 3) {
82+
logger.warn("Malformed authorization header");
83+
return self.fail(buildWwwAuthenticateHeader(requiredScopes, ERROR.INVALID_TOKEN), 401);
84+
}
85+
86+
// Validate second header component is a valid access_token
87+
var accessTokenString = authHeaderComponents[1];
88+
89+
// Decode and validate access_token
90+
TokenUtil.decodeAndValidate(accessTokenString, this.serviceConfig.getOAuthServerUrl()).then(function (accessToken) {
91+
if (!accessToken) {
92+
logger.warn("Invalid access_token");
93+
return self.fail(buildWwwAuthenticateHeader(requiredScopes, ERROR.INVALID_TOKEN), 401);
5694
}
57-
58-
// Validate that first header component is Bearer
59-
var authHeaderComponents = authHeader.split(" ");
60-
if (authHeaderComponents[0].indexOf(BEARER) !== 0) {
61-
logger.warn("Malformed authorization header");
62-
return self.fail(buildWwwAuthenticateHeader(requiredScope, ERROR.INVALID_TOKEN), 401);
95+
// Validate token contains required scopes
96+
const requiredScopesArray = requiredScopes.split(" ").filter(scope => scope !== "");
97+
const suppliedScopesArray = accessToken.scope.split(" ");
98+
for (const requiredScope of requiredScopesArray) {
99+
if (!suppliedScopesArray.includes(requiredScope)) {
100+
logger.warn("access_token does not contain required scope. Expected ::", requiredScopes, " Received ::",
101+
accessToken.scope);
102+
return self.fail(buildWwwAuthenticateHeader(requiredScopes, ERROR.INSUFFICIENT_SCOPE), 401);
103+
}
63104
}
64105

65-
// Validate header has exactly 2 or 3 components (Bearer, access_token, [id_token])
66-
if (authHeaderComponents.length !== 2 && authHeaderComponents.length !== 3) {
67-
logger.warn("Malformed authorization header");
68-
return self.fail(buildWwwAuthenticateHeader(requiredScope, ERROR.INVALID_TOKEN), 401);
106+
// validate the audience section
107+
if (!accessToken.aud) {
108+
return self.fail(buildWwwAuthenticateHeader("access token missing audience section", ERROR.INVALID_TOKEN), 401);
69109
}
70-
71-
// Validate second header component is a valid access_token
72-
var accessTokenString = authHeaderComponents[1];
73-
74-
// Decode and validate access_token
75-
TokenUtil.decodeAndValidate(accessTokenString, this.serviceConfig.getOAuthServerUrl()).then(function (accessToken) {
76-
if (!accessToken) {
77-
logger.warn("Invalid access_token");
78-
return self.fail(buildWwwAuthenticateHeader(requiredScope, ERROR.INVALID_TOKEN), 401);
110+
if (!Array.isArray(accessToken.aud)) {
111+
return self.fail(buildWwwAuthenticateHeader("access token malformed audience array", ERROR.INVALID_TOKEN), 401);
112+
}
113+
if (options.audience && options.audience.trim()) {
114+
// audience is an array (currently we support only 1 item in the array)
115+
const requestAudience = options.audience.trim();
116+
const requiredList = requestAudience.split(' ');
117+
if (requiredList.length > 1) {
118+
return self.fail(buildWwwAuthenticateHeader("multiple audiences are not supported", ERROR.INVALID_REQUEST), 400);
79119
}
80-
81-
// Validate token contains required scopes
82-
var requiredScopeElements = requiredScope.split(" ");
83-
var suppliedScopeElements = accessToken.scope.split(" ");
84-
for (var i = 0; i < requiredScopeElements.length; i++) {
85-
var requiredScopeElement = requiredScopeElements[i];
86-
var found = false;
87-
for (var j = 0; j < suppliedScopeElements.length; j++) {
88-
var suppliedScopeElement = suppliedScopeElements[j];
89-
if (requiredScopeElement === suppliedScopeElement) {
90-
found = true;
91-
break;
92-
}
93-
}
94-
if (!found) {
95-
logger.warn("access_token does not contain required scope. Expected ::", requiredScope, " Received ::",
96-
accessToken.scope);
97-
return self.fail(buildWwwAuthenticateHeader(requiredScope, ERROR.INSUFFICIENT_SCOPE), 401);
98-
}
120+
const tokenAudience = accessToken.aud;
121+
if (!tokenAudience.includes(requestAudience)) {
122+
return self.fail(buildWwwAuthenticateHeader("audience mismatch. expected:" + tokenAudience +
123+
" got:" + requestAudience, ERROR.INSUFFICIENT_SCOPE), 401);
99124
}
100-
101-
req.appIdAuthorizationContext = {
102-
accessToken: accessTokenString,
103-
accessTokenPayload: accessToken
104-
};
105-
106-
// Decode and validate id_token
107-
var identityTokenString;
108-
var identityToken;
109-
if (authHeaderComponents.length === 3) {
110-
identityTokenString = authHeaderComponents[2];
111-
TokenUtil.decodeAndValidate(identityTokenString, self.serviceConfig.getOAuthServerUrl()).then(function (identityToken) {
112-
if (identityToken) {
113-
req.appIdAuthorizationContext.identityToken = identityTokenString;
114-
req.appIdAuthorizationContext.identityTokenPayload = identityToken;
115-
} else {
116-
logger.warn("Invalid identity_token. Proceeding with access_token only");
117-
}
118-
logger.debug("authentication success");
119-
return self.success(identityToken || null);
120-
}).catch(() => {
121-
logger.debug("authentication failed due to invalid identity token");
122-
return self.fail(buildWwwAuthenticateHeader(requiredScope, ERROR.INVALID_TOKEN), 401);
123-
});
125+
}
126+
127+
req.appIdAuthorizationContext = {
128+
accessToken: accessTokenString,
129+
accessTokenPayload: accessToken
130+
};
131+
132+
// Decode and validate id_token
133+
var identityTokenString;
134+
var identityToken;
135+
if (authHeaderComponents.length === 3) {
136+
identityTokenString = authHeaderComponents[2];
137+
TokenUtil.decodeAndValidate(identityTokenString, self.serviceConfig.getOAuthServerUrl()).then(function (identityToken) {
138+
if (identityToken) {
139+
req.appIdAuthorizationContext.identityToken = identityTokenString;
140+
req.appIdAuthorizationContext.identityTokenPayload = identityToken;
124141
} else {
125-
logger.debug("authentication success: identity_token not found. Proceeding with access_token only");
126-
return self.success(identityToken || null);
142+
logger.warn("Invalid identity_token. Proceeding with access_token only");
127143
}
128-
}).catch(function () {
129-
logger.debug("authentication failed due to invalid access token");
130-
return self.fail(buildWwwAuthenticateHeader(requiredScope, ERROR.INVALID_TOKEN), 401);
131-
});
132-
133-
// .success(user, info) - call on auth success. user=object, info=object
134-
// .fail(challenge, status) - call on auth failure. challenge=string, status=int
135-
// .redirect(url, status) - call on redirect required. url=url, status=int
136-
// .pass() - skip strategy processing
137-
// .error(err) - error during strategy processing. err=Error obj
144+
logger.debug("authentication success");
145+
return self.success(identityToken || null);
146+
}).catch(() => {
147+
logger.debug("authentication failed due to invalid identity token");
148+
return self.fail(buildWwwAuthenticateHeader(requiredScopes, ERROR.INVALID_TOKEN), 401);
149+
});
150+
} else {
151+
logger.debug("authentication success: identity_token not found. Proceeding with access_token only");
152+
return self.success(identityToken || null);
153+
}
154+
}).catch(function () {
155+
logger.debug("authentication failed due to invalid access token");
156+
return self.fail(buildWwwAuthenticateHeader(requiredScopes, ERROR.INVALID_TOKEN), 401);
157+
});
158+
159+
// .success(user, info) - call on auth success. user=object, info=object
160+
// .fail(challenge, status) - call on auth failure. challenge=string, status=int
161+
// .redirect(url, status) - call on redirect required. url=url, status=int
162+
// .pass() - skip strategy processing
163+
// .error(err) - error during strategy processing. err=Error obj
138164
};
139165

140166
function buildWwwAuthenticateHeader(scope, error) {
141-
var msg = BEARER + " scope=\"" + scope + "\"";
142-
if (error) {
143-
msg += ", error=\"" + error + "\"";
144-
}
145-
return msg;
167+
var msg = BEARER + " scope=\"" + scope + "\"";
168+
if (error) {
169+
msg += ", error=\"" + error + "\"";
170+
}
171+
return msg;
146172
}
147173

148174
module.exports = ApiStrategy;

lib/strategies/webapp-strategy.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,4 +551,36 @@ WebAppStrategy.prototype.logoutSSO = function (req, res, options = {}) {
551551
res.redirect(url);
552552
};
553553

554+
/**
555+
* Returns true if the token on the request's session has the required scopes.
556+
* @param req: The request containing the App ID token.
557+
* @param requiredScopes {String}: Scope names (not fullName) separated by spaces. For example: 'write read update'.
558+
* @returns {boolean}
559+
*/
560+
WebAppStrategy.hasScope = function (req, requiredScopes) {
561+
if (typeof requiredScopes !== 'string' || !requiredScopes.trim()) {
562+
logger.error('requiredScopes is either empty or not a string');
563+
return true;
564+
}
565+
if (!req || !req.session || !req.session[WebAppStrategy.AUTH_CONTEXT] ||
566+
!req.session[WebAppStrategy.AUTH_CONTEXT].accessTokenPayload ||
567+
typeof req.session[WebAppStrategy.AUTH_CONTEXT].accessTokenPayload.scope !== 'string') {
568+
logger.warn('access token scope could not be found on request\'s session');
569+
return false;
570+
}
571+
572+
const suppliedScopes = req.session[WebAppStrategy.AUTH_CONTEXT].accessTokenPayload.scope;
573+
// get the required scopes as an array
574+
const requiredScopesArray = requiredScopes.split(" ").filter(scope => scope !== ""); // split by spaces and ignore empty required scopes
575+
// get the supplied scopes as an array while removing the prefix (app URI) since the required scopes aren't prefixed:
576+
const suppliedScopesArray = suppliedScopes.split(" ").map(fullScope => fullScope.split("/").pop());
577+
578+
for (const requiredScope of requiredScopesArray) {
579+
if (!suppliedScopesArray.includes(requiredScope)) {
580+
return false;
581+
}
582+
}
583+
return true;
584+
};
585+
554586
module.exports = WebAppStrategy;

0 commit comments

Comments
 (0)