How to configure multi realm in nestjs nest-keycloak-connect library? - nestjs

They ask to pass this configuration, but it doesn't explain how it works, and since I use token from 2 different realms I can't authenticate. Can someone explain to me how to do all the configuration
authServerUrl: 'http://localhost:8180/auth',
clientId: 'nest-api',
secret: 'fallback',
returns null
multiTenant: {
realmResolver: (request) => {
return request.get('host').split('.')[0];
},
realmSecretResolver: (realm) => {
const secrets = { master: 'secret', slave: 'password' };
return secrets[realm];
}
}
}

Related

Passing JWT to UI application after Google Oauth2 login

I have created a MERN application with a separate backend and frontend. I have added support for Google Oauth2 login using passport-google-oauth20 npm package.
So I have exposed an end point in the backend as follows:
class AccountAPIs {
constructor() { }
redirectToSuccess(req, res) {
const accountServiceInst = AccountService.getInst();
let account = req.session.passport.user;
let filePath = path.join(__dirname + '../../../public/views/loginSuccess.html');
let jwt = accountServiceInst.generateJWT(account);
// how do I send this jwt to ui application
res.sendFile(filePath);
}
loadMappings() {
return {
'/api/auth': {
'/google': {
get: {
callbacks: [
passport.authenticate('google', { scope: ['profile', 'email'] })
]
},
'/callback': {
get: {
callbacks: [
passport.authenticate('google', { failureRedirect: '/api/auth/google/failed' }),
this.redirectToSuccess
]
}
},
'/success': {
get: {
callbacks: [this.successfulLogin]
}
}
}
}
};
}
}
Here is the passport setup for reference:
let verifyCallback = (accessToken, refreshToken, profile, done) => {
const accountServiceInst = AccountService.getInst();
return accountServiceInst.findOrCreate(profile)
.then(account => {
return done(null, account);
})
.catch(err => {
return done(err);
});
};
let googleStrategyInst = new GoogleStrategy({
clientID: serverConfig.auth.google.clientId,
clientSecret: serverConfig.auth.google.clientSecret,
callbackURL: 'http://localhost/api/auth/google/callback'
}, verifyCallback);
passport.use(googleStrategyInst);
In the UI application, on button click I am opening a new window which opens the '/api/auth/google' backend API. After authenticating with a google account, the window redirects to the '/api/auth/google/callback' backend API where I am able to generate a JWT. I am unsure about how to transfer this JWT to the frontend application since this is being opened in a separate window.
I know that res.cookie('jwt', jwt) is one way to do it. Please suggest the best practices here..
There are two ways to pass the token to the client :
1- you put the token into a cookie as you have mentioned
2-you pass the token in the redirect URL to the client as a parameter "CLIENT_URL/login/token", that you can extract the token in your front-end client

Secure a GraphQL API with passport + JWT's or sessions? (with example)

To give a bit of context: I am writing an API to serve a internal CMS in React that requires Google login and a React Native app that should support SMS, email and Apple login, I am stuck on what way of authentication would be the best, I currently have an example auth flow below where a team member signs in using Google, a refresh token gets sent in a httpOnly cookie and is stored in a variable in the client, then the token can be exchanged for an accessToken, the refresh token in the cookie also has a tokenVersion which is checked before sending an accessToken which does add some extra load to the database but can be incremented if somebody got their account stolen, before any GraphQL queries / mutations are allowed, the user's token is decoded and added to the GraphQL context so I can check the roles using graphql-shield and access the user for db operations in my queries / mutations if needed
Because I am still hitting the database even if it's only one once on page / app load I wonder if this is a good approach or if I would be better off using sessions instead
// index.ts
import "./passport"
const main = () => {
const server = fastify({ logger })
const prisma = new PrismaClient()
const apolloServer = new ApolloServer({
schema: applyMiddleware(schema, permissions),
context: (request: Omit<Context, "prisma">) => ({ ...request, prisma }),
tracing: __DEV__,
})
server.register(fastifyCookie)
server.register(apolloServer.createHandler())
server.register(fastifyPassport.initialize())
server.get(
"/auth/google",
{
preValidation: fastifyPassport.authenticate("google", {
scope: ["profile", "email"],
session: false,
}),
},
// eslint-disable-next-line #typescript-eslint/no-empty-function
async () => {}
)
server.get(
"/auth/google/callback",
{
preValidation: fastifyPassport.authorize("google", { session: false }),
},
async (request, reply) => {
// Store user in database
// const user = existingOrCreatedUser
// sendRefreshToken(user, reply) < send httpOnly cookie to client
// const accessToken = createAccessToken(user)
// reply.send({ accessToken, user }) < send accessToken
}
)
server.get("/refresh_token", async (request, reply) => {
const token = request.cookies.fid
if (!token) {
return reply.send({ accessToken: "" })
}
let payload
try {
payload = verify(token, secret)
} catch {
return reply.send({ accessToken: "" })
}
const user = await prisma.user.findUnique({
where: { id: payload.userId },
})
if (!user) {
return reply.send({ accessToken: "" })
}
// Check live tokenVersion against user's one in case it was incremented
if (user.tokenVersion !== payload.tokenVersion) {
return reply.send({ accessToken: "" })
}
sendRefreshToken(user, reply)
return reply.send({ accessToken: createAccessToken(user) })
})
server.listen(port)
}
// passport.ts
import fastifyPassport from "fastify-passport"
import { OAuth2Strategy } from "passport-google-oauth"
fastifyPassport.registerUserSerializer(async (user) => user)
fastifyPassport.registerUserDeserializer(async (user) => user)
fastifyPassport.use(
new OAuth2Strategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: "http://localhost:4000/auth/google/callback",
},
(_accessToken, _refreshToken, profile, done) => done(undefined, profile)
)
)
// permissions/index.ts
import { shield } from "graphql-shield"
import { rules } from "./rules"
export const permissions = shield({
Mutation: {
createOneShopLocation: rules.isAuthenticatedUser,
},
})
// permissions/rules.ts
import { rule } from "graphql-shield"
import { Context } from "../context"
export const rules = {
isAuthenticatedUser: rule()(async (_parent, _args, ctx: Context) => {
const authorization = ctx.request.headers.authorization
if (!authorization) {
return false
}
try {
const token = authorization.replace("Bearer", "")
const payload = verify(token, secret)
// mutative
ctx.payload = payload
return true
} catch {
return false
}
}),
}
To answer your question directly, you want to be using jwts for access and that's it. These jwts should be created tied to a user session, but you don't want to have to manage them. You want a user identity aggregator to do it.
You are better off removing most of the code to handle user login/refresh and use a user identity aggregator. You are running into common problems of the complexity when handling the user auth flow which is why these exist.
The most common is Auth0, but the price and complexity may not match your expectations. I would suggest going through the list and picking the one that best supports your use cases:
Auth0
Okta
Firebase
Cognito
Authress
Or you can check out this article which suggests a bunch of different alternatives as well as what they focus on

MSAL - use two different clientid from different directory

I have an application that will be used by two different entities and each entity have their own Azure Active Directory.
Initially, the code I am using is:
var msalConfig = {
auth: {
clientId: '<client-id-1>'
authority: "https://login.microsoftonline.com/<tenant-id>"
},
cache: {
cacheLocation: "localStorage",
storeAuthStateInCookie: true
}
};
Now what I want to happen is, can I put two different client ID, and tenant ID?
I could use multiple tenant in the first AAD, but I want to limit it to only two tenants. What should be my approach here?
You could try using the Factory pattern and create a method that will instantiate the clientApplication for the proper client. For example:
const msalConfigFoo = {
auth: {
clientId: '<client-id-1>'
authority: "https://login.microsoftonline.com/<tenant-id>"
},
cache: {
cacheLocation: "localStorage",
storeAuthStateInCookie: true
}
};
var msalConfigBar = {
auth: {
clientId: '<client-id-2>'
authority: "https://login.microsoftonline.com/<tenant-id>"
},
cache: {
cacheLocation: "localStorage",
storeAuthStateInCookie: true
}
};
function getClientApplication(clientType) {
if (clientType == "foo") {
return new Msal.UserAgentApplication(msalConfigFoo);
} else {
return new Msal.UserAgentApplication(msalConfigBar);
}
}
Question: I could use multiple tenant in the first AAD, but I want to limit it to only two tenants. What should be my approach here?
Answer: if you develop a multiple-tenant AD application, you can validate "id_token" with its issuer after your users log in. For example :
var msalConfig = {
auth: {
clientId: 'b0114608-677e-4eca-ae22-60c32e1782d9', //This is your client ID
authority: "https://login.microsoftonline.com/common" //This is your tenant info
},
cache: {
cacheLocation: "localStorage",
storeAuthStateInCookie: true
}
};
var graphConfig = {
graphMeEndpoint: "https://graph.microsoft.com/v1.0/me"
};
// create a request object for login or token request calls
// In scenarios with incremental consent, the request object can be further customized
var requestObj = {
scopes: ["user.read"]
};
var myMSALObj = new Msal.UserAgentApplication(msalConfig);
// Register Callbacks for redirect flow
// myMSALObj.handleRedirectCallbacks(acquireTokenRedirectCallBack, acquireTokenErrorRedirectCallBack);
myMSALObj.handleRedirectCallback(authRedirectCallBack);
// difine issuers
var issuers = new Array();
issuers[0]="https://login.microsoftonline.com/{TenantId}/v2.0";
issuers[1]="https://login.microsoftonline.com/{TenantId}/v2.0";
function signIn() {
myMSALObj.loginPopup(requestObj).then(idToken => {
var issuer =String(idToken.idToken["issuer"])
console.log(issuer)
if(issuers.indexOf(issuer) != -1){
//login successfully then your users can do otherthing
}else{
// your users use a wrong account
}
}).catch(function (error) {
//Please check the console for errors
console.log(error);
});
}
For more details, please refer to the document

Azure AD open BearerStrategy "TypeError: self.success is not a function"

I asked this at https://github.com/AzureAD/passport-azure-ad/issues/427 but have had no response. I feel it is a bug that is preventing me from completing my work so I am reaching farther and wider to get an answer. Is it something I'm doing or a bug?
I am writing the question different to I had before since I have done some more investigation in to the problem (I will indicate where the differences start).
Passport-Azure-AD version 4.1.0 - https://www.npmjs.com/package/passport-azure-ad#52-bearerstrategy
I have set this up from the documentation:
setup() {
const findById = (id, fn) => {
for (let i = 0, len = this.users.length; i < len; i++) {
const user = this.users[i];
if (user.sub === id) {
logger.info('Found user: ', user);
return fn(null, user);
}
}
return fn(null, null);
};
this.bearerStrategy = new BearerStrategy(jwtOptions,
(token: ITokenPayload, done: VerifyCallback) => {
findById(token.oid, (err, user) => {
if (err) {
return done(err);
}
if (!user) {
// 'Auto-registration'
logger.info('User was added automatically as they were new. Their oid is: ', token.oid);
this.users.push(token);
this.owner = token.oid;
return done(null, token);
}
this.owner = token.oid;
return done(null, user, token);
});
}
);
console.log(`setup bearerStrategy`);
}
The jwtOptions I use are:
The options are like this:
const jwtOptions = {
identityMetadata: 'https://login.microsoftonline.com/xyz/v2.0/.well-known/openid-configuration',
clientID: '0123456789',
loggingLevel: 'info',
loggingNoPII: false,
passReqToCallback: false
};
And run authentication (from middlewhere) using the following:
authenticate(request: express.Request) {
this.bearerStrategy.authenticate(request, {session: false});
}
NOTE That this is different to the doco since what they had doesn't work.
It fails on the line:
return done(null, token);
With:
[2019-05-29T13:49:33.479] [INFO ] [AUTHSERVICE_LOGGER] - User was added automatically as they were new. Their oid is: 123
.../translateboard/node_modules/passport-azure-ad/lib/bearerstrategy.js:565
return self.success(user, info);
^
TypeError: self.success is not a function
at verified (/Users/bbos/dev/dhs/translate/translateboard/node_modules/passport-azure-ad/lib/bearerstrategy.js:565:21)
at findById (/Users/bbos/dev/dhs/translate/translateboard/server/src/services/AuthService.ts:106:32)
at findById (/Users/bbos/dev/dhs/translate/translateboard/server/src/services/AuthService.ts:87:20)
at Strategy.bearerStrategy.passport_azure_ad_1.BearerStrategy [as _verify] (/Users/bbos/dev/dhs/translate/translateboard/server/src/services/AuthService.ts:97:17)
at jwt.verify (/Users/bbos/dev/dhs/translate/translateboard/node_modules/passport-azure-ad/lib/bearerstrategy.js:363:19)
at /Users/bbos/dev/dhs/translate/translateboard/node_modules/passport-azure-ad/lib/jsonWebToken.js:80:16
at process._tickCallback (internal/process/next_tick.js:61:11)
From here is different compared to original post
If I put a breakpoint in the code, the self Object in BearerStrategy.js where the error is:
{
"name": "oauth-bearer",
"_options": {
"identityMetadata": "https://login.microsoftonline.com/xyz/v2.0/.well-known/openid-configuration",
"clientID": "0123456789",
"loggingLevel": "info",
"loggingNoPII": false,
"passReqToCallback": false,
"clockSkew": 300,
"validateIssuer": true,
"allowMultiAudiencesInToken": false,
"audience": [
"1234",
"spn:1234"
],
"isB2C": false,
"_isCommonEndpoint": false,
"_verify" = (token, done) => {...},
"__proto__" = Strategy(...,
}
}
And under __proto__ are:
authenticate = function authenticateStrategy(req, options) {
constructor = function Strategy(options, verifyFn) {
failWithLog = function(message) {
jwtVerify = function jwtVerifyFunc(req, token, metadata, optionsToValidate, done) {
loadMetadata = function(params, next) {
You can see there is no success in Passport-Azure-Ad. It does define a failWithLog https://github.com/AzureAD/passport-azure-ad/blob/e9684341920ac8ac41c55a1e7150d1765dced809/lib/bearerstrategy.js#L600 - did they forget to add the others?
Passport defines these others (https://github.com/jaredhanson/passport/blob/1c8ede35a334d672024e14234f023a87bdccaac2/lib/middleware/authenticate.js#L230) however they are in a closure and never exposed. Nor is the parent Strategy object that they are defined on. The only connection with the outside is through the exposed authenticate method https://github.com/jaredhanson/passport/blob/1c8ede35a334d672024e14234f023a87bdccaac2/lib/middleware/authenticate.js#L70
However as seen, Passport-Azure-Ad defines it's own authenticate method (https://github.com/AzureAD/passport-azure-ad/blob/e9684341920ac8ac41c55a1e7150d1765dced809/lib/bearerstrategy.js#L372) and never calls the passsport one.
To me it looks like it never worked.
Can anyone confirm or disagree?
I will update the post at https://github.com/AzureAD/passport-azure-ad/issues/427 to refer to this.
Next I am going to git bisect the repository to see if I can find a change where those missing methods used to be defined or something else that stands out.
I can confirm that as my code was written it would never work. There were two main issues:
Pass parameters
As per my comment against the question, i neglected to provide information in the question since I didn't think it was relevant. But it is.
I am using TSED - TypeScript Express Decorators (https://tsed.io) and it replaces express middleware code like:
server.get('/api/tasks', passport.authenticate('oauth-bearer', { session: false }), listTasks);
With an annotated middleware class - https://tsed.io/docs/middlewares.html
So now the call to passport.authenticate() is in the use() method like this as I showed before (THIS IS INCORRECT):
#OverrideMiddleware(AuthenticatedMiddleware)
export class UserAuthMiddleware implements IMiddleware {
constructor(#Inject() private authService: AuthService) {
}
public use(
#EndpointInfo() endpoint: EndpointMetadata,
#Request() request: express.Request,
#Response() response: express.Response,
#Next() next: express.NextFunction
) {
const options = endpoint.get(AuthenticatedMiddleware) || {};
Passport.authenticate('oauth-bearer', {session: false}); // <-- WRONG
if (!request.isAuthenticated()) {
throw new Forbidden('Forbidden');
}
next();
}
}
What I neglected to consider is that express middleware is passed the request object. So what I actually needed to have was:
Passport.authenticate('oauth-bearer', {session: false})(request, response, next); // <-- CORRECT
Must use Passport.use()
The documentation is misleading. Given I'm not overly savy with Passport I didn't think much about this.
The doco (http://www.passportjs.org/packages/passport-azure-ad/) (at 5.2.1.1 Sample using the BearerStrategy) says to use:
var bearerStrategy = new BearerStrategy(options,
function(token, done) {
log.info('verifying the user');
log.info(token, 'was the token retreived');
findById(token.oid, function(err, user) {
if (err) {
return done(err);
}
if (!user) {
// "Auto-registration"
log.info('User was added automatically as they were new. Their oid is: ', token.oid);
users.push(token);
owner = token.oid;
return done(null, token);
}
owner = token.oid;
return done(null, user, token);
});
}
);
I am aware that when other strategies are described (such as the 5.1 OIDCStrategy on the same page):
passport.use(new OIDCStrategy({
identityMetadata: config.creds.identityMetadata,
clientID: config.creds.clientID,
...
},
function(iss, sub, profile, accessToken, refreshToken, done) {
...
}
));
They use passport.use. I thought about the difference (when i first saw it) for 1/2 a second and concluded that the AAD BearerStrategy handles things differently given the login is done by Azure using their msal.js library. And I didn't revisit this until Fix # 1 above didn't fix the problem.
I conclude that the TSED project needs to update their documentation / samples (I will do this for them); and the Passport Azure AD project needs to update their doco.
There are still some issues and I don't know who is at fault. I wrote about these at Passport-Azure-Ad in TSED framework seems to run asynchronously.

In firebase, create a custom token with specific exp?

I notice that the docs specify that I can create a token to expire up to 3600 seconds later[1] But I don't see how to do that with auth().createCustomToken ... I can manually do it with jsonwektoken, but it seems like this should be addressable directly with firebase-admin library.
Another question is, what is the secret I need to verify my own token generated in this way, the uid ?
index.js
// demo server generating custom auth for firebase
import Koa from 'koa'
import Koajwt from 'koa-jwt'
import Token from './token'
const app = new Koa()
// Custom 401 handling if you don't want to expose koa-jwt errors to users
app.use(function(ctx, next){
return next().catch((err) => {
if (401 == err.status) {
ctx.status = 401
ctx.body = 'Protected resource, use Authorization header to get access\n'
} else {
throw err
}
})
})
// Unprotected middleware
app.use(function(ctx, next){
if (ctx.url.match(/^\/login/)) {
// use router , post, https to securely send an id
const conf = {
uid: 'sample-user-uid',
claims: {
// Optional custom claims to include in the Security Rules auth / request.auth variables
appid: 'sample-app-uid'
}
}
ctx.body = {
token: Token.generateJWT(conf)
}
} else {
return next();
}
});
// Middleware below this line is only reached if JWT token is valid
app.use(Koajwt({ secret: 'shared-secret' }))
// Protected middleware
app.use(function(ctx){
if (ctx.url.match(/^\/api/)) {
ctx.body = 'protected\n'
}
})
app.listen(3000);
token.js
//import jwt from 'jsonwebtoken'
import FirebaseAdmin from 'firebase-admin'
import serviceAccount from 'demo-admin-firebase-adminsdk-$$$$-$$$$$$.json'
export default {
isInitialized: false,
init() {
FirebaseAdmin.credential.cert(serviceAccount)
isInitialized = true
},
/* generateJWTprimiative (payload, signature, conf) {
// like: jwt.sign({ data: 'foobar' }, 'secret', { expiresIn: '15m' })
jwt.sign(payload, signature, conf)
} */
generateJWT (conf) {
if(! this.isInitialized)
init()
FirebaseAdmin.auth().createCustomToken(conf.uid, conf.claims)
.then(token => {
return token
})
.catch(err => {
console.log('no token generate because', err)
})
}
}
[1] https://firebase.google.com/docs/auth/admin/create-custom-tokens
You can't change the token expiration. The docs you found includes the words:
Firebase tokens comply with the OpenID Connect JWT spec, which means
the following claims are reserved and cannot be specified within the
additional claims:
... exp ...
This is further backed up by inspecting the Firebase Admin SDK source code on GitHub.
In this section:
public createCustomToken(uid: string, developerClaims?: {[key: string]: any}): Promise<string> {
// .... cut for length ....
const header: JWTHeader = {
alg: ALGORITHM_RS256,
typ: 'JWT',
};
const iat = Math.floor(Date.now() / 1000);
const body: JWTBody = {
aud: FIREBASE_AUDIENCE,
iat,
exp: iat + ONE_HOUR_IN_SECONDS,
iss: account,
sub: account,
uid,
};
if (Object.keys(claims).length > 0) {
body.claims = claims;
}
// .... cut for length ....
You can see the exp property is hard coded to be iat + ONE_HOUR_IN_SECONDS where the constant is defined elsewhere in the code as 60 * 60...
If you want to customize the expiration time, you will HAVE to create your own token via a 3rd party JWT package.
To your 2nd question, a secret is typically stored in the server environment variables, and is a pre-set string or password. Technically you could use the UID as the secret, but that would be a TERRIBLE idea security wise - please don't do this. Your secret should be like your password, keep it secure and don't upload it with your source code to GitHub. You can read more about setting and retrieving environment variables in Firebase in these docs here

Resources