Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Passport authentication with JWTStrategy not working - Error: Could not get a response #153

Closed
akshaydotsh opened this issue Mar 26, 2018 · 35 comments

Comments

@akshaydotsh
Copy link

Hello,
I was trying to authenticate with passport JWTStrategy

passport-oauth.js :

const passport = require('passport');
const JwtStrategy = require('passport-jwt').Strategy,
    ExtractJwt = require('passport-jwt').ExtractJwt;

const oauth = require('../oauth/oauth_credentials');
const User = require('../models/user');

var opts = {};
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
opts.secretOrKey = require('./jwt').secret;

console.log('Obviously control comes here');
passport.use(new JwtStrategy(opts, function (jwt_payload, done) {
    console.log('Control never comes here');
    User.findOne({ _id: jwt_payload.sub }, function (err, user) {
        if (err) {
            return done(err, false);
        }
        if (user) {
            return done(null, user);
        } else {
            return done(null, false);
        }
    });
}));

passport.serializeUser(function (user, done) {
    done(null, user._id);
});

If you see my two console.log() functions, the control never comes to passport.use(new JWTStrategy...

I am authenticating here :

router.post('/test', passport.authenticate('jwt', { session: false }), (req, res) => {
    res.send('Authenticated');
});

I used postman to send my request :
screen shot 2018-03-26 at 11 48 07 pm

As you can see I provided the authentication header too , and I am getting this error Could not get a response .
If anyone could please tell me what's wrong, I've been at it since hours.

Thanks.

@kittrCZ
Copy link

kittrCZ commented May 24, 2018

Hi @theakshaygupta , have you ever been able to resolve this? I'm running to exactly same issue :(

@akshaydotsh
Copy link
Author

Hi @kittrCZ ,
It didn't work for the dummy project I was practicing but then in the main project, all I did was changed the
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
to:
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderWithScheme('JWT');

Also if you're logging in the user, you must be using jwt.sign() to generate the token. After this step you're probably either storing the token somewhere or you're sending the token in response. Make sure that you're sending as such
token: 'JWT ' + token
I think using lowercase jwt causes problems sometimes.

I don't know if this will work but that's all the change I could see in my previous code and earlier code.
So it boils down to three things :

  • Check the extractor you are using. Sometimes weird discrepancies occur there

  • Check if you're appending the token with 'JWT' and not 'jwt'

  • There could also be a version issue. Check the version of passport-jwt, passport and jsonwebtoken for any changes or incompatibility

Let's discuss if you're still running into the same problem.

Thanks

@kittrCZ
Copy link

kittrCZ commented May 24, 2018

This is pretty frustrating. I'm sure that I overlooked something, but I can't digest that the documented way is not working. Totally agree with your suggestions and have it implemented similarly, I have following:

passport.js

const passport = require('passport');
const passportJWT = require('passport-jwt');
const models = require('../models');
const LocalStrategy = require('passport-local').Strategy;

const ExtractJWT = require('passport-jwt').ExtractJwt;
const JWTStrategy = require('passport-jwt').Strategy;

passport.use(new LocalStrategy({
	usernameField: 'email',
	passwordField: 'password',
	passReqToCallback: true,
}, async (req, username, password, done) => {
	// this one is typically a DB call.
	// Assume that the returned user object is pre-formatted and ready for storing in JWT
	try {
		const errMessage = { message: 'Incorrect email or password' };

		const user = await models.user.findOne({
			where: {
				email: username,
			},
		});

		if (!user) {
			return done(null, false, errMessage);
		}

		const pwdValidation = await user.validPassword(password, user.password);
		if (!pwdValidation) {
			done(null, false, errMessage);
		} else {
			done(null, user, { message: 'Logged In Successfully' });
		}
	} catch (err) {
		console.log(err);
		done(err);
	}
}));

const opts = {};
opts.jwtFromRequest = ExtractJWT.fromAuthHeaderWithScheme('JWT');
opts.secretOrKey = process.env.API_JWT_SECRET;
passport.use(new JWTStrategy(opts, async (jwtPayload, done) => {
	console.log(jwtPayload);
	try {
		const user = await models.user.findOne({
			where: {
				email: jwtPayload.email,
			},
		});

		if (user) {
			return done(null, user);
		}

		return done(null, false);
	} catch (e) {
		console.log(e);
		done(e);
	}
}));

// In order to help keep authentication state across HTTP requests,
// Sequelize needs to serialize and deserialize the user
// Just consider this part boilerplate needed to make it all work
passport.serializeUser((user, cb) => {
	cb(null, user.id);
});

passport.deserializeUser((id, cb) => {
	models.user.findOne(id, (err, user) => {
		cb(err, user);
	});
});

route.js

router.get('/test', passport.authenticate('jwt', { session: false }), (req, res) =>{
	const response = {
		success: true,
	};

	return res.status(200).json(response);
});

package.json

  "dependencies": {
    "bcrypt": "^2.0.1",
    "body-parser": "^1.18.2",
    "connect-timeout": "^1.9.0",
    "cors": "^2.8.4",
    "express": "^4.16.3",
    "express-rate-limit": "^2.11.0",
    "jsonwebtoken": "^8.2.1",
    "lodash": "^4.17.10",
    "moment": "^2.22.1",
    "nodemon": "^1.17.2",
    "passport": "^0.4.0",
    "passport-jwt": "^4.0.0",
    "passport-local": "^1.0.0",
    "pg": "^7.4.3",
    "pg-hstore": "^2.3.2",
    "response-time": "^2.3.2",
    "sequelize": "^4.37.7",
    "slugify": "^1.3.0",
    "validator": "^10.2.0"
  }

and this command does not work.

curl

$ curl -v -XGET -H 'Authorization:JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRAMi5jb20iLCJpYXQiOjE1MjcxNDk5NDEsImV4cCI6MTUyNzE0OTk0NH0.Y_6K15LpWycwJg_TZXjKh7JVUACYciuLqxR3VaI2xUw' 'http://localhost:4000/users/test'
Note: Unnecessary use of -X or --request, GET is already inferred.
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 4000 (#0)
> GET /users/test HTTP/1.1
> Host: localhost:4000
> User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)
> Accept: */*
> Referer: 
> Authorization:JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRAMi5jb20iLCJpYXQiOjE1MjcxNDk5NDEsImV4cCI6MTUyNzE0OTk0NH0.Y_6K15LpWycwJg_TZXjKh7JVUACYciuLqxR3VaI2xUw
> 
< HTTP/1.1 401 Unauthorized
< Vary: Origin
< X-RateLimit-Limit: 100
< X-RateLimit-Remaining: 95
< X-Response-Time: 1008.037ms
< Date: Thu, 24 May 2018 08:20:44 GMT
< Connection: keep-alive
< Content-Length: 12
< 
* Connection #0 to host localhost left intact
Unauthorized% 

any clue @theakshaygupta , I stop believing that this package even works ^^

@akshaydotsh
Copy link
Author

I assume your console.log(jwtPayload) is not printing anything ?

@kittrCZ
Copy link

kittrCZ commented May 24, 2018

@theakshaygupta no totally nothing. Everything gets instant 401 response

@akshaydotsh
Copy link
Author

@kittrCZ Sorry, I am unable to figure out what is going wrong here. The only problem I can see here is that your token is expired. You could try to re create the token and provide it in the header.
If it doesn't work, I hope someone more familiar to passport-jwt responds to this issue .

Cheers

@kittrCZ
Copy link

kittrCZ commented May 24, 2018

so I figured out yesterday. The token was truly expired. I highly recommend to everyone to create a
testing endpoint with jwt.verify().

This issue ca be closed

@arnasledev
Copy link

@kittrCZ was your token testing endpoint included just after it was created? I'm facing the same issue but I'm not sure if I'm using it properly. Mine looks like this and it stands just before generating res.send

const token = jwt.sign(user, '4waNdqInzkWHwTs4BXXp9ZAsolK0EV75', {expiresIn: 86400 * 30});
jwt.verify(token, '4waNdqInzkWHwTs4BXXp9ZAsolK0EV75', function(err, data){
     console.log(err, data);
})

return res.send({
                message: 'Logged In Successfully!',
                redirect: '/dashboard',
                jwtToken: 'JWT ' + token,
                success: true,
                user: {
                    id: user.id,
                    name: user.user_name
                }
            });

@holumyn
Copy link

holumyn commented Jun 12, 2018

Hi all, i'm still having this challenge after verification using jwt.verify(), token was successfully verified. Strangely, it didnt work
let opts = {}
opts.jwtFromRequest = req.headers.authorization.split(' ')[1];
opts.secretOrKey = process.env.JWT_TOKEN;
opts.ignoreExpiration = true;

 passport.use(new JwtStrategy(opts, function(jwt_payload, done) {
  
      console.log('Now in middleware' ); //Not logging
      User.findOne({_id: jwt_payload._id}, function(err, user) {
          if (err) {
          
              return done(err, false);
          }
          if (user) {
              return done(null, user);
          } else {
              return done(null, false);
          }
      });
  }));
 First, i have to fetch the token using a different method. Also, could someone tell me why i can't console.log in passport.use(...{...}).

Digging further, i did
let pass = passport.use(......
console.log(pass);
and i got this

{"_key":"passport","_strategies":{"session":{"name":"session"},"jwt":{"name":"jwt","_jwtFromRequest":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1YjFkNmQwMWI5N2YxMTAxZDU4MDY1MmQiLCJmaXJzdE5hbWUiOiJoZWxvbyIsInJvbGUiOlsiUGF0aWVudCJdLCJleHAiOjE1Mjk0MDYxMjIsInNlY3JldE9yS2V5IjoiUkVTVEZVTEFQSXMiLCJpYXQiOjE1Mjg4MDEzMjJ9.KTminBQ8Mzi7IuKsjKjtWPSvYWevqHRW","_verifOpts":{"ignoreExpiration":true}}},"_serializers":[null],"_deserializers":[],"_infoTransformers":[],"_framework":{},"_userProperty":"user","_sm":{"_key":"passport"},"strategies":{}}

It actually got the data. Not really sure what is going on at this point again.

@darinrogers
Copy link

darinrogers commented Aug 15, 2018

Any updates on this? It seems like the original poster had an expiration issue, but others like @arnasledev and @holumyn haven't found solutions.

Here's an update of my own: I wasn't setting issuer and audience claims correctly.

@arnasledev
Copy link

Mine problem was different secret keys on passport and verification of the login.

@mohammadalijf
Copy link

hey guys,
i had the same problem and solved the problem by writing a little middleware

// passport.js
passport.use(new JWTStrategy({
    jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
    secretOrKey: process.env.SERVERSECRET
}, (token, done) => {
    return done(null, token);
}));
// router/index.js

/**
 * middleware for checking authorization with jwt
 */
function authorized(request, response, next) {
    passport.authenticate('jwt', { session: false, }, async (error, token) => {
        if (error || !token) {
            response.status(401).json({ message: 'Unauthorized' });
        } 
        try {
            const user = await User.findOne({
                where: { id: token.id },
            });
            request.user = user;
        } catch (error) {
            next(error);
        }
        next();
    })(request, response, next);   
}

router.use('/user', authorized, userRouter);
// router/user.js

router.get('/', (request, response, next) => {
    response.send(request.user);
});

and in my request i had to set Authorization: bearer

@ButeForce
Copy link

I had the same issue , it turned out when using ExtractJwt.fromAuthHeaderAsBearerToken() we have to send send the authorization header as bearer + token , although in the authentication for the route we write : passport.authenticate('jwt', { session: false, }, function()...etc.

this applies to the new version of passport where the original function ExtractJwt.fromAuthHeader() is not available anymore.

@rajat1saxena
Copy link

rajat1saxena commented Feb 17, 2019

In case you are still stuck, follow the following link for resolution.

#117 (comment)

@DipendraPoudel
Copy link

DipendraPoudel commented Oct 5, 2019

I had the same issue . The imported model reference in one of the method in passport js file was not correct.After i correct that the problem solved

@fromage9747
Copy link

I am currently experiencing this issue SUDDENLY. I have been working on my app for two years and never had an issue and now suddenly I start getting an issue. All my routes that use passport.authenticate now no longer work.

@EmmyMay
Copy link

EmmyMay commented Oct 29, 2019

I had the same issue , it turned out when using ExtractJwt.fromAuthHeaderAsBearerToken() we have to send send the authorization header as bearer + token , although in the authentication for the route we write : passport.authenticate('jwt', { session: false, }, function()...etc.

this applies to the new version of passport where the original function ExtractJwt.fromAuthHeader() is not available anymore.

This worked for me. Turns out that when using

opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken()

You have to return "Bearer + token" and not "JWT + token". I did that and it worked for me. Deep sigh. Finally!

@EmmyMay
Copy link

EmmyMay commented Oct 29, 2019

I am currently experiencing this issue SUDDENLY. I have been working on my app for two years and never had an issue and now suddenly I start getting an issue. All my routes that use passport.authenticate now no longer work.

Things have changed. For example, you can no longer use ExtractJwt.fromAuthHeader() function anymore. It is deprecated. Just compare your code with the latest examples, and you will have your app working again.

@fromage9747
Copy link

@EmmyMay Thanks EmmyMay. It would appear that I am experiencing a different issue and have raised it as such. #188

@JohnnyHandy
Copy link

I had the same issue , it turned out when using ExtractJwt.fromAuthHeaderAsBearerToken() we have to send send the authorization header as bearer + token , although in the authentication for the route we write : passport.authenticate('jwt', { session: false, }, function()...etc.
this applies to the new version of passport where the original function ExtractJwt.fromAuthHeader() is not available anymore.

This worked for me. Turns out that when using

opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken()

You have to return "Bearer + token" and not "JWT + token". I did that and it worked for me. Deep sigh. Finally!

How do you set the header with the token then? Do you use res.set or something like it?

@JohnnyHandy
Copy link

hey guys,
i had the same problem and solved the problem by writing a little middleware

// passport.js
passport.use(new JWTStrategy({
    jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
    secretOrKey: process.env.SERVERSECRET
}, (token, done) => {
    return done(null, token);
}));
// router/index.js

/**
 * middleware for checking authorization with jwt
 */
function authorized(request, response, next) {
    passport.authenticate('jwt', { session: false, }, async (error, token) => {
        if (error || !token) {
            response.status(401).json({ message: 'Unauthorized' });
        } 
        try {
            const user = await User.findOne({
                where: { id: token.id },
            });
            request.user = user;
        } catch (error) {
            next(error);
        }
        next();
    })(request, response, next);   
}

router.use('/user', authorized, userRouter);
// router/user.js

router.get('/', (request, response, next) => {
    response.send(request.user);
});

and in my request i had to set **Authorization: bearer **

Please can you show how did you set your request with this "Authorization:bearer"?

@lucasnzd
Copy link

Make sure the value you are using on the jwt.sign() is a number or if you are using process.env parse it to a number.

@shekhar123sajwan
Copy link

You have to return "Bearer + token" and not "JWT + token". Add bearer "token" in Authorization value

@JohnnyHandy
Copy link

JohnnyHandy commented Dec 15, 2019

You have to return "Bearer + token" and not "JWT + token". Add bearer "token" in Authorization value

Yes... Look I have been struggling with it for more than three days... There is any code example on how to do the request to a protected route and send the header with these values? I have been able to do it on postman but not in my application. I would be really glad if I had an example on how to do it properly.
The issue is adressed here #190

@eyoeldefare
Copy link

Having same issue. Just tried to test it and doesn't work

@eyoeldefare
Copy link

You have to return "Bearer + token" and not "JWT + token". Add bearer "token" in Authorization value

Wow, that worked surprisingly. How did you come to figure that out? I am just confused about how you came to have that information? Why was it changed from JWT to Bearer and was not mentioned in the documentation?

@mikenicholson
Copy link
Owner

Why was it changed from JWT to Bearer and was not mentioned in the documentation?

It's mentioned here: https://github.com/mikenicholson/passport-jwt/blob/master/docs/migrating.md#migrating-from-2xx-to-3xx

@fromage9747
Copy link

fromage9747 commented Feb 17, 2020

Well after much investigation I believe I have found the source of my problem as well as a better understanding of how JWT works. The JWT seems to have gotten too large. My functioning user accounts have 6500 or fewer characters whilst my user accounts that no longer work are in the area of 8500. Now it's onto trying to figure out how to configure passport-jwt to only store a much smaller amount of data.

For me it was this code when logging in:
const token = jwt.sign(user.toJSON(), config.secret, { expiresIn: 604800 //1 week });

image

The problem was "user". I was saving the ENTIRE user document from mongodb which had quite a bit of data in it. Data that was completely unnecessary. So I created a new object with the data that I actually needed and created the JWT with that.

const userJWT = { user_id: user._id, full_name: user.full_name, username: user.username, email_address: user.email_address }; const token = jwt.sign(userJWT, config.secret, { expiresIn: 604800 //1 week });

image

I included images as the formatting of the code was not working as I wanted it to in the Github comment editor.

@daveteu
Copy link

daveteu commented May 19, 2020

Well after much investigation I believe I have found the source of my problem as well as a better understanding of how JWT works. The JWT seems to have gotten too large. My functioning user accounts have 6500 or fewer characters whilst my user accounts that no longer work are in the area of 8500. Now it's onto trying to figure out how to configure passport-jwt to only store a much smaller amount of data.

For me it was this code when logging in:
const token = jwt.sign(user.toJSON(), config.secret, { expiresIn: 604800 //1 week });

image

The problem was "user". I was saving the ENTIRE user document from mongodb which had quite a bit of data in it. Data that was completely unnecessary. So I created a new object with the data that I actually needed and created the JWT with that.

const userJWT = { user_id: user._id, full_name: user.full_name, username: user.username, email_address: user.email_address }; const token = jwt.sign(userJWT, config.secret, { expiresIn: 604800 //1 week });

image

I included images as the formatting of the code was not working as I wanted it to in the Github comment editor.

a little off topic but JWT should not store all your information like names, email address etc as JWT can be decoded by anybody. You wouldn't want anybody to know the ownership of the token, it's a security risk.

You should just store non-sensitive information e.g. userid.

The userid can then be used to retrieve information from your DB without again verifying that userid and password matches.

@fromage9747
Copy link

@daveteu Thanks!

@elonaire
Copy link

If you are following the documentation for NestJS, something seems to have been left out. Kindly make sure that you are also passing the secret during signing. I have mine in my .env file, thus the code snippet below:
return { access_token: this.jwtService.sign(payload, {secret: ${process.env.SECRET}}), };

@naima-shk
Copy link

Hi all, I have been facing the same issue lately . Here's a detailed description of my issue. Someone, please help me out ..https://stackoverflow.com/questions/66091341/jwt-authentication-failed?noredirect=1#comment117182238_66091341

@Werayootk
Copy link

hey guys, i had the same problem and solved the problem by writing a little middleware

// passport.js
passport.use(new JWTStrategy({
    jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
    secretOrKey: process.env.SERVERSECRET
}, (token, done) => {
    return done(null, token);
}));
// router/index.js

/**
 * middleware for checking authorization with jwt
 */
function authorized(request, response, next) {
    passport.authenticate('jwt', { session: false, }, async (error, token) => {
        if (error || !token) {
            response.status(401).json({ message: 'Unauthorized' });
        } 
        try {
            const user = await User.findOne({
                where: { id: token.id },
            });
            request.user = user;
        } catch (error) {
            next(error);
        }
        next();
    })(request, response, next);   
}

router.use('/user', authorized, userRouter);
// router/user.js

router.get('/', (request, response, next) => {
    response.send(request.user);
});

and in my request i had to set **Authorization: bearer **

Thx you

@roshen1234
Copy link

hi my jwt is not getting called even my console.log({ jwt_payload }); is not getting called
i have spent lot of time to correct it but cant find the error somebody pls

const express = require('express');
const server = express();
const mongoose = require('mongoose');
const cors = require('cors');
const session = require('express-session');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const crypto = require('crypto');
const jwt = require('jsonwebtoken');
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
const { createProduct } = require('./controller/Product');
const productsRouter = require('./routes/Products');
const categoriesRouter = require('./routes/Category');
const brandsRouter = require('./routes/Brands');
const usersRouter = require('./routes/User');
const authRouter = require('./routes/Auth');
const cartRouter = require('./routes/Cart');
const ordersRouter = require('./routes/Order');
const { User } = require('./model/user');
const { isAuth, sanitizeUser } = require('./services/common');
const port=8080;

const SECRET_KEY = 'SECRET_KEY';
// JWT options
const opts = {};
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
opts.secretOrKey = SECRET_KEY; // TODO: should not be in code;

//middlewares

server.use(
session({
secret: 'keyboard cat',
resave: false, // don't save session if unmodified
saveUninitialized: false, // don't create session until something stored
})
);
server.use(passport.authenticate('session'));
server.use(
cors({
exposedHeaders: ['X-Total-Count'],
})
);
server.use(express.json()); // to parse req.body
server.use('/products', isAuth(), productsRouter.router);
// we can also use JWT token for client-only auth
server.use('/categories', isAuth(), categoriesRouter.router);
server.use('/brands', isAuth(), brandsRouter.router);
server.use('/users', isAuth(), usersRouter.router);
server.use('/auth', authRouter.router);
server.use('/cart', isAuth(), cartRouter.router);
server.use('/orders', isAuth(), ordersRouter.router);

// Passport Strategies
passport.use(
'local',
new LocalStrategy(async function (username, password, done) {
// by default passport uses username
try {
const user = await User.findOne({ email: username });
console.log(username, password, user);
if (!user) {
return done(null, false, { message: 'invalid credentials' }); // for safety
}
crypto.pbkdf2(
password,
user.salt,
310000,
32,
'sha256',
async function (err, hashedPassword) {
if (!crypto.timingSafeEqual(user.password, hashedPassword)) {
return done(null, false, { message: 'invalid credentials' });
}
const token = jwt.sign(sanitizeUser(user), SECRET_KEY);
done(null, token); // this lines sends to serializer
}
);
} catch (err) {
done(err);
}
})
);

passport.use(
'jwt',
new JwtStrategy(opts, async function (jwt_payload, done) {
console.log({ jwt_payload });
try {
const user = await User.findOne({ id: jwt_payload.sub });
if (user) {
return done(null, sanitizeUser(user)); // this calls serializer
} else {
return done(null, false);
}
} catch (err) {
return done(err, false);
}
})
);

// this creates session variable req.user on being called from callbacks
passport.serializeUser(function (user, cb) {
console.log('serialize', user);
process.nextTick(function () {
return cb(null, { id: user.id, role: user.role });
});
});

// this changes session variable req.user when called from authorized request

passport.deserializeUser(function (user, cb) {
console.log('de-serialize', user);
process.nextTick(function () {
return cb(null, user);
});
});

const mongoURI="mongodb://0.0.0.0/ecommerce"
mongoose.connect(mongoURI)
const conn=mongoose.connection
conn.once('open',()=>{
console.log('successfullly connected to database')
})
conn.once('error',(error)=>{
console.log(failed to connected to database${error.message})
})
server.listen(port, () => {
console.log(Ecommerce backend listening at http://localhost:${port})
})

@Sanafan
Copy link

Sanafan commented Feb 8, 2024

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests