Skip to content
This repository has been archived by the owner on Apr 22, 2024. It is now read-only.

Commit

Permalink
Merge pull request #466 from dylanmcreynolds/feature/oidc
Browse files Browse the repository at this point in the history
Enhance server.js to load passport loginCallbacks . Thanks for the effort !
  • Loading branch information
stephan271 authored Jul 16, 2021
2 parents 5a84347 + 238449a commit d634076
Show file tree
Hide file tree
Showing 8 changed files with 324 additions and 13 deletions.
27 changes: 14 additions & 13 deletions CI/ALS/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
# gives a docker image below 200 MB
FROM mhart/alpine-node:10
FROM node:14-alpine
RUN apk update && apk upgrade

RUN apk add --update python build-base git
ENV NODE_ENV "production"
ENV PORT 3000
EXPOSE 3000
# create local user to avoid running as root
RUN addgroup mygroup && adduser -D -G mygroup myuser && mkdir -p /usr/src/app && chown -R myuser /usr/src/app

# Prepare app directory
WORKDIR /usr/src/app
COPY package*.json ./
COPY .snyk ./
WORKDIR /home/node/app
COPY package*.json /home/node/app/
COPY .snyk /home/node/app/

USER myuser

RUN npm install
# set up local user to avoid running as root
# RUN chown -R node:node /home/node/app
# USER node

# Install our packages
RUN npm ci --production
RUN npm config set registry http://registry.npmjs.org/
RUN npm config set strict-ssl false
RUN npm ci --only=production

# Copy the rest of our application, node_modules is ignored via .dockerignore
COPY . /usr/src/app
COPY . /home/node/app


# Start the app
CMD ["node", "."]
CMD ["node", "."]
16 changes: 16 additions & 0 deletions CI/ALS/dev/overrides/datasources.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"mongo": {
"host": "mongodb",
"port": 27017,
"url": "",
"database": "dacat",
"name": "mongo",
"connector": "mongodb",
"useNewUrlParser": true,
"allowExtendedOperators":true
},
"transient": {
"name": "transient",
"connector": "transient"
}
}
40 changes: 40 additions & 0 deletions CI/ALS/dev/overrides/functionalAccounts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
[{
"account": "admin",
"password": "2jf0TPNZsS",
"email": "[email protected]",
"role": "admin",
"global": true
}, {
"account": "ingestor",
"password": "aman",
"email": "[email protected]",
"role": "ingestor",
"global": true
}, {
"account": "archiveManager",
"password": "aman",
"email": "[email protected]",
"role": "archivemanager"

}, {
"account": "proposalIngestor",
"password": "aman",
"email": "[email protected]",
"role": "proposalingestor"
}, {
"account": "synmx",
"password": "synmx",
"email": "[email protected]",
"role": "ingestor"
}, {
"account": "syncsaxs",
"password": "syncsaxs",
"email": "[email protected]",
"role": "ingestor"
}, {
"account": "syntomcat",
"password": "syntomcat",
"email": "[email protected]",
"role": "ingestor"
}
]
158 changes: 158 additions & 0 deletions CI/ALS/dev/overrides/login-callbacks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
"use strict";

/*
This module communicates with ALSHub at two different points in the login process.
The first is an observer on the UserIdentity model that adds group information to the user's
profile from ALSHub. This gets used by Catamel to enforce access controls.
The second is a loginCallback for passport that checks that the user is a user in ALSHub.
If not, it sends blank credentials to the return of the loginCallback, prompting
loopback-passport to not log the user in.
*/





/*
A loginCallback is function used by passport that gives one a chance
to intercept the login message flow. This is useful in cases where the
an OAuth2/OIDC provider is a third part (like ORCID), but an internal system
must be queried to add information to the user's profile.
With a generic passport implementation, one could simple attach the callback
funtion as a member of the passport configuration. However, Catamel uses the
loopback-passport-confgigurator, which is configed via a .json file (providers.json)
and not through a .js file. This means that the entry in the is at best a string, not a
function.
server.js will
- import this file
- read the loginCallback configuration
- if the string matches a function defined in this file, it will attach that function
to the provider.
Below is an example of a callback function. This is example mimics the default that passport
configures if none has been configured. Note a few important things:
- the function is exported through module.exports
- the function calls done(err, user, authInfo)
- if a custom call back decides that a user should not be logged in, call done(none, none, none)
module.exports.sampleLoginCallback = function(req, done) {
return function(err, user, identity, token) {
var authInfo = {
identity: identity,
};
if (token) {
authInfo.accessToken = token;
}
done(err, user, authInfo);
};
};
*/

const logger = require("../../common/logger");
var request = require("request");

const authenticators = {
ORCID: "orcid",
GOOGLE: "google"
};

const reqIDFields = {
ORCID: "orcid",
EMAIL: "email"
};

function getUserURL(userIdentity){
let idField = "";
let subjectId = "";
if (userIdentity.provider == authenticators.ORCID){
idField = reqIDFields.ORCID;
subjectId = userIdentity.profile._json.sub;
}
else if (userIdentity.provider == authenticators.GOOGLE){
idField = reqIDFields.EMAIL;
subjectId = userIdentity.profile._json.email;
}
else{
return null;
}
return `${process.env.USER_SVC_API_URL}${subjectId}/${idField}?api_key=${process.env.USER_SVC_API_KEY}`;
}

// Observe saving UserItentity. This gives us the ability to update the user profile with facitly-specific groups
module.exports = function (app) {
app.models.UserIdentity.observe("before save", function(ctx, next) {
if (!ctx.data){
logger.logInfo("No context data from UserItentity");
next();
return;
}
const userURL = getUserURL(ctx.currentInstance);
if (!userURL){
logger.logError(`unexpected authenticator type: ${ctx.currentInstance.provider}`);
next();
return;
}
request(userURL, function (error, response, _body) {
// ask ALSHub for user information so we can get group info
if (error){
logger.logError(`error talking to splash_userservice ${error.message}`);
next();
return;
}
if (response.statusCode == 200){
// add groups to profile, saving in the UserItentity model
ctx.data.profile.accessGroups = JSON.parse(response.body).groups;
}
next();
});
});
};


// ALS Login callback, registered with Passport. This gives us the opportunity
// to deny login if the user is not found in the facility directory.
module.exports.alsLoginCallback = function(req, done) {
return function(err, user, identity, token) {

var authInfo = {
identity: identity,
};
if (token) {
authInfo.accessToken = token;
}

const requestURL = getUserURL(identity);
if (!requestURL){
logger.logError(`unexpected authenticator type: ${identity.provider}`);
done();
return;
}
request(requestURL, function (error, response, body) {
// ask ALSHub for the user's information
if (error){
logger.logError(`error talking to splash_userservice ${error.message}`);
done(err, null, null);
return;
}
if (response.statusCode == 200) {
const bodyObj = JSON.parse(body);
logger.logInfo("user service returned", bodyObj);
}
else{
logger.logError(`error returned from splash_userservice ${response.statusCode} - ${body}.`);
// user couldn't be found, prevent login by sending nulls
done(err, null, null);
return;
}
done(err, user, authInfo);
});
};
};
35 changes: 35 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"amqplib": "^0.8.0",
"compression": "^1.7.4",
"cors": "^2.8.5",
"express-session": "^1.17.2",
"gelf-pro": "^1.3.4",
"helmet": "^3.23.3",
"jsonwebtoken": "^8.5.1",
Expand Down
38 changes: 38 additions & 0 deletions server/boot/login-callbacks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
A loginCallback is function used by passport that gives one a chance
to intercept the login message flow. This is useful in cases where the
an OAuth2/OIDC provider is a third part (like ORCID), but an internal system
must be queried to add information to the user's profile.
With a generic passport implementation, one could simple attach the callback
funtion as a member of the passport configuration. However, Catamel uses the
loopback-passport-confgigurator, which is configed via a .json file (providers.json)
and not through a .js file. This means that the entry in the is at best a string, not a
function.
server.js will
- import this file
- read the loginCallback configuration
- if the string matches a function defined in this file, it will attach that function
to the provider.
Below is an example of a callback function. This is example mimics the default that passport
configures if none has been configured. Note a few important things:
- the function is exported through module.exports
- the function calls done(err, user, authInfo)
- if a custom call back decides that a user should not be logged in, call done(none, none, none)
module.exports.sampleLoginCallback = function(req, done) {
return function(err, user, identity, token) {
var authInfo = {
identity: identity,
};
if (token) {
authInfo.accessToken = token;
}
done(err, user, authInfo);
};
};
*/
Loading

0 comments on commit d634076

Please sign in to comment.