My purpose is to secure my backend, which is deployed as GCP App Engine, with an API-Gateway, provided by Google, to only let Firebase authenticated users have access to it.
I set everything up, as stated in the documents:
API-Gateway config:
{
"swagger": "2.0",
"info": {
"title": "PROJECT_NAME",
"version": "1.0"
},
"securityDefinitions": {
"firebase": {
"authorizationUrl": "",
"flow": "implicit",
"type": "oauth2",
"x-google-issuer": "https://securetoken.google.com/PROJECT_ID",
"x-google-jwks_uri": "https://www.googleapis.com/service_accounts/v1/metadata/x509/securetoken#system.gserviceaccount.com",
"x-google-audiences": "PROJECT_ID"
}
},
"security": [
{
"firebase": [ ]
}
],
"paths": {
"/hello-spring": {
"get": {
"produces": [
"application/json"
],
"operationId": "helloSpring",
"parameters": [],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
},
"x-google-backend": {
"address": "https://URI-TO-APP-ENGINE-SERVICE",
"path_translation": "APPEND_PATH_TO_ADDRESS"
"jwt_audience": "API_CREDENTIALS_FROM_IAP_APP_ENGINE_APP"
}
}
}
},
"definitions": {
"User": {
"properties": {
"creationTimestamp": {
"format": "date-time",
"type": "string"
},
"id": {
"format": "int32",
"type": "integer"
},
"name": {
"type": "string"
},
"updateTimestamp": {
"format": "date-time",
"type": "string"
},
"uuid": {
"type": "string"
}
},
"type": "object"
}
},
"x-components": {}
}
When I send a request - without a jwt token - to this api-gateways url, it correctly responds with
{
"message": "Jwt is missing",
"code": 401
}
Firebase jwt
"Default" firebase backend is used, no custom tokens are created. Firebase is used to create the JWT token. From the client following code snippet is used to receive a valid JWT:
final FirebaseAuth _auth = FirebaseAuth.instance;
....
final GoogleSignInAccount googleSignInAccount = await googleSignIn.signIn();
final GoogleSignInAuthentication googleSignInAuthentication = await googleSignInAccount.authentication;
final AuthCredential credential = GoogleAuthProvider.credential(
accessToken: googleSignInAuthentication.accessToken,
idToken: googleSignInAuthentication.idToken,
);
final UserCredential authResult = await _auth.signInWithCredential(credential);
final String token = await _auth.currentUser.getIdToken();
But as soon as I add the JWT (final String token in code above), which is created by a firebase client app and add it to the request header with
Authorization: Bearer TOKEN_CODE
gateway responds with
{
"code": 401,
"message": "Jwt verification fails"
}
The most detailed LOG I can see in GCP is:
response_code_detail: jwt_authn_access_denied{Jwt_verification_fails}
I also created a service account linked to this API-Gateway with following roles:
roles/appengine.appViewer
roles/iam.serviceAccountUser
roles/run.invoker
roles/apigateway.viewer
But still, the same error: "401 - Jwt verification fails". What am I missing,how can I see more details about this issue in GCP and what could be a solution?
Related
I am using Firebase Authentication with Identify Platform and am trying to add custom claims when a user is created. I am looking at this example from Google's website here: Setting custom and session claims:
exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
if (context.credential &&
context.credential.providerId === 'saml.my-provider-id') {
return {
// Employee ID does not change so save in persistent claims (stored in
// Auth DB).
customClaims: {
eid: context.credential.claims.employeeid,
},
// Copy role and groups to token claims. These will not be persisted.
sessionClaims: {
role: context.credential.claims.role,
groups: context.credential.claims.groups,
}
}
}
});
The code is straight forward. I am trying to add custom claims for all new users but they are not getting set. I am not sure how else to to try. This is my actual code:
exports.beforeUserCreate = functions.auth.user().beforeCreate((user, context) => {
functions.logger.info('Attempting to set claims for new user', user);
functions.logger.info('Here is the context', context);
return {
customClaims: {
roles: ['user'],
},
sessionClaims: {
roles: ['user'],
},
};
});
I do see the logs in the Google console, so I know my function is being called. I also tested the claims without the array like roles: 'TestRole', but that didn't work either. The user object just does not have the custom claims.
If I manually set the claims they do show up as expected:
{
"roles": [
"admin",
"subscriber",
"superadmin"
],
"iss": "https://securetoken.google.com/...",
"aud": "xxx",
"auth_time": 1661813313,
"user_id": "xxxx",
"sub": "xxx",
"iat": 1661813313,
"exp": 1661816913,
"email": "xxx",
"email_verified": false,
"firebase": {
"identities": {
"email": [
"xx"
]
},
"sign_in_provider": "password"
}
}
This is what the user object looks like when I try to create the claims automatically:
{
"iss": "https://securetoken.google.com/...",
"aud": "xxx",
"auth_time": 1661813351,
"user_id": "xxx",
"sub": "xxx",
"iat": 1661813351,
"exp": 1661816951,
"email": "xxx",
"email_verified": false,
"firebase": {
"identities": {
"email": [
"xxx"
]
},
"sign_in_provider": "password"
}
}
Also, I tried setting both customClaims and sessionClaims independently. Neither show up on the user object, nor are the custom claims saved for the user.
One more update. I tried setting the display name in beforeCreate and that worked.
return {
customClaims: {
roles: 'pie',
},
displayName: 'pie',
};
// RESULT:
{
"name": "pie",
"iss": "https://securetoken.google.com/...",
"aud": "xxx",
"auth_time": 1661816987,
"user_id": "xxx",
"sub": "xxx",
"iat": 1661816987,
"exp": 1661820587,
"email": "xxx",
"email_verified": false,
"firebase": {
"identities": {
"email": [
"xxx"
]
},
"sign_in_provider": "password"
}
}
From Darwin in comments:
Hi #Gremash , there's an open github issue regarding that. See sessionClaims content not getting added to the decoded token. Also, there's a fix that has been recently merged regarding this issue.
The session is stored in cookies. When I use this {session ? `${session.user.email}` : ''} it keeps saying undefined.
This is my JSON object:
{
"user": {
"token": {
"responseCode": 0,
"responseMessage": "success",
"data": {
"id": 6,
"userId": "SYS-9d502c43-9ef3-432d-ab93-be0b8236cdff",
"fullName": "yeshewas string",
"username": "yes#yes.com",
"userRole": "Super User",
"permissions": []
}
}
},
"expires": "2022-06-24T06:14:39.011Z"
}
Try to change your jwt callback like this:
callbacks: {
jwt: async ({ token, user }) => {
user && (token.user = user.user)
return token;
},
},
I am working with Meteor User accounts to create users. I have implemented two ways of creating users.
By using accounts-password to create (default one ).
OAuth Services (accounts-google and accounts-facebook)
A user account generated with accounts-password have the document shown below
{
"_id": "DQnDpEag2kPevSdJY",
"createdAt": "2015-12-10T22:34:17.610Z",
"services": {
"password": {
"bcrypt": "XXX"
},
"resume": {
"loginTokens": [
{
"when": "2015-12-10T22:34:17.615Z",
"hashedToken": "XXX"
}
]
}
},
-----
----
}
Where as a user account generated with accounts-google or account-facebook have the document shown below.
{
"_id": "Ap85ac4r6Xe3paeAh",
"createdAt": "2015-12-10T22:29:46.854Z",
"services": {
"facebook": {
"accessToken": "XXX",
"expiresAt": 1454970581716,
"id": "XXX",
"email": "myname#gmail.com",
"name": "Ada Lovelace",
"first_name": "Ada",
"last_name": "Lovelace",
"link": "https://www.facebook.com/app_scoped_user_id/XXX/",
"gender": "female",
"locale": "en_US",
"age_range": {
"min": 21
}
},
---
---
---
Now the real issue is, Although the email address used is same for both accounts-password and accounts-google (in my case email is myname#gmail.com), two different user accounts are being created.
I am looking for solution Something like below. (Note: Services has both "Password" and "Facebook" sections under single account)
{
"_id": "DQnDpEag2kPevSdJY",
"createdAt": "2015-12-10T22:34:17.610Z",
"services": {
"password": {
"bcrypt": "XXX"
},
"facebook": {
"accessToken": "XXX",
"expiresAt": 1454970581716,
"id": "XXX",
"email": "myname#gmail.com",
"name": "Ada Lovelace",
"first_name": "Ada",
"last_name": "Lovelace",
"link": "https://www.facebook.com/app_scoped_user_id/XXX/",
"gender": "female",
"locale": "en_US",
"age_range": {
"min": 21
}
},
},
-----
----
}
Is there a way where only one account is being generated in both cases, means if a user is already existed and the same is trying with OAuth service, first account should be used to accommodate the service ?
link-accounts package is the recommended way to allow users to add additional services to their account.
You can use Accounts.setPassword on the server in order to generate a proper bcrypt hash for the accounts:
Accounts.setPassword('Ap85ac4r6Xe3paeAh', 'the-new-password')
which will result in
{
"_id": "Ap85ac4r6Xe3paeAh",
"createdAt": "2015-12-10T22:29:46.854Z",
"services": {
"password": {
"bcrypt": "$2b$10$nzHCivxVqxbuFBBPWewPPu.r5x7OR5gJB8PIklU4OoU.WK0MT8jt2"
},
"facebook": {
"accessToken": "XXX",
"expiresAt": 1454970581716,
"id": "XXX",
"email": "myname#gmail.com",
"name": "Ada Lovelace",
"first_name": "Ada",
"last_name": "Lovelace",
"link": "https://www.facebook.com/app_scoped_user_id/XXX/",
"gender": "female",
"locale": "en_US",
"age_range": {
"min": 21
}
}
}
}
I have solved the above issue with a hack.
In imports/startup/server/accounts.js I have added the below validation logic which always validates the newly created account.
The idea is, this process checks if user is already existed in database. If the user exists, further checks if its created from accounts-password or accounts-google/facebook .
Based on the existing type modify the existing fields with new fields and throw an error with a fancy message (This actually prevents the new account to be created).
Accounts.validateNewUser(function (user) {
// first check what is the newly creating service
var service =
user.services.google || user.services.facebook || user.services.password;
if (!service) return true;
var existingUser = null;
// due to some issues both `Meteor.users.findOne(email)` as well `Account.findUserByEmail(email)` methods have been used to find the existing user status
if (user.services.password) {
var email = user.emails[0].address;
existingUser = Meteor.users.findOne({
$or: [
{ "registered_emails[0].address": email },
{ "services.google.email": email },
{ "services.facebook.email": email },
],
});
} else {
var email = service.email;
//console.log(" retrieved email ", email);
existingUser = Accounts.findUserByEmail(email);
}
//console.log(" existingUser : ", existingUser);
if (!existingUser) return true;
if (user.services.google) {
Meteor.users.update(
{ _id: existingUser._id },
{
$set: {
profile: user.profile,
"services.google": user.services.google,
},
}
);
} else if (user.services.facebook) {
Meteor.users.update(
{ _id: existingUser._id },
{
$set: {
profile: user.profile,
"services.facebook": user.services.facebook,
},
}
);
} else {
Meteor.users.update(
{ _id: existingUser._id },
{
$set: {
profile: user.profile,
"services.password": user.services.password,
"services.email": user.services.email,
emails: user.emails,
},
}
);
}
throw new Meteor.Error(
205,
"Merged with your existing Social Login accounts now. Try refresh the page and sign in again. That should work !!"
);
});
I'm using braintree for payment and I have done this.
gateway.customer.create({
firstName: "Sachin",
lastName: "Shah",
company: "Qwerty",
email: "Qwerty#example.com",
phone: "114.555.1234",
fax: "614.555.1234",
website: "www.example.com",
}, function (err, result) {
if (err) {
res.send({code:0, status:'Error', message:err});
}else{
res.send({code:1, status:'Success', data: result});
}
});
I followed it's official doc and they show that when req is success I'll get token but I'm gtting result.customer.paymentMethods[]
Response
{
"code": 1,
"status": "Success",
"data": {
"customer": {
"id": "569549779",
"merchantId": "XXXXXXXXXXXXXXXXX",
"firstName": "Sachin",
"lastName": "Shah",
"company": "Qwerty",
"email": "Qwerty#example.com",
"phone": "114.555.1234",
"fax": "614.555.1234",
"website": "www.example.com",
"createdAt": "2019-10-10T05:13:42Z",
"updatedAt": "2019-10-10T05:13:42Z",
"customFields": "",
"globalId": "XXXXXXXXXXXXXXXXX",
"creditCards": [],
"addresses": [],
"paymentMethods": []
},
"success": true
}
}
Expected Output
I need to get paymentMethodToken for further API calls.
Full disclosure: I work at Braintree. If you have any further questions, feel free to contact support.
You're currently creating a customer without a payment method. You'll need to create the customer with a payment method to retrieve a paymentMethodToken.
I have a basic auth secured API but after filling in the authentication credentials, it does not apply to request header. I saw"ERROR Server not found or an error occurred " at swagger editor and "401 Unauthorized" on fiddler.
User Name and Pwd : odata and qtkr47PTM3pmzLyEHNrW4DXhhgyjMfM3CKUZfXdn0tk=
Here is my swagger json
{
"swagger": "2.0",
"info": {
"version": "1.0.0",
"title": "Basic Auth Example",
"description": "An example for how to use Basic Auth with Swagger.\nServer code is available [here](http://navm3.cloudapp.net:90/nav/odata). It's running on NAVM3.\n\n**You can use below User Name and Password for test.**\n* User Name: `ODATA`\n* Password: `qtkr47PTM3pmzLyEHNrW4DXhhgyjMfM3CKUZfXdn0tk=`\n"
},
"host": "navm3.cloudapp.net:90",
"basePath": "/nav/odata",
"schemes": [
"http"
],
"securityDefinitions": {
"basicAuth": {
"type": "basic",
"description": "HTTP Basic Authentication. Works over `HTTP` and `HTTPS`"
}
},
"paths": {
"/": {
"get": {
"security": [
{
"basicAuth": []
}
],
"responses": {
"200": {
"description": "Will send `Authenticated` if authentication is succesful, otherwise it will send `Unauthorized`"
}
}
}
}
}
}
i had the same problem, but the problem was in nodeJS back-end.
If you are using NodeJS, probably the problem it is with CORS. You should enable CORS in NodeJS with Express and everything will work.
To enable the CORS in nodeJS you can add the below code, before of the routes.
var app = express();
app.use(function(req, res, next) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, UPDATE, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
next();
});
try to replace the following part:
"securityDefinitions": {
"basicAuth": {
"type": "basic",
"description": "HTTP Basic Authentication. Works over `HTTP` and `HTTPS`"
}
},
with the following
"securityDefinitions": {
"basicAuth": {
"type": "http",
"scheme": "basic"
}
},
keeping the rest as is
here is the reference https://swagger.io/docs/specification/authentication/basic-authentication/