I'm trying to verify a JWT token in a middleware on Next.js (v12.3.2) but am getting errors with jsonwebtoken (following the docs here: https://clerk.dev/docs/request-authentication/validate-session-tokens) because it requires a Node environment.
I've tried using jose instead with the CLERK_JWT_KEY for Clerk.dev but I keep getting a [Error: Key info doesn't have required parameters] error. For reference, this is what my code looks like:
export const decodeAndVerifyToken = async (
getToken: ServerGetToken
): Promise<JWTPayload | undefined> => {
// initialize a variable for the token
let token: string | null;
try {
// get the token using metadata template, which should return
// a 'publicMetadata' object containing an 'isAdmin' value
token = await getToken({ template: "metadata" });
} catch (err) {
// if we had an error getting the token, return undefined
return undefined;
}
// if no token is found, then short-circuit to undefined
if (!token) {
return undefined;
}
// split the jwt key to 64-bit lines
const splitPem = process.env.CLERK_JWT_KEY?.match(/.{1,64}/g) ?? [];
// combine into a public key format
const publicKey =
"-----BEGIN PUBLIC KEY-----\n" +
splitPem.join("\n") +
"\n-----END PUBLIC KEY-----";
//
const test = await importSPKI(publicKey, "ES256").catch((err) =>
console.log(err)
);
};
I've also tried directly calling const decoded = await jwtVerify(token, publicKey); but that also produces errors.
Anyone know how to address?
As per the example key in clerk.dev these are RSA keys you're importing. That key is not a valid key for ES256 that you pass to importSPKI.
I suspect the algorithm will be RS256 but to be sure inspect the token's header to confirm.
Looking at the docs they do have a JWKs endpoint so this might be a wholelot easier for you.
import * as jose from 'jose'
const JWKS = jose.createRemoteJWKSet(new URL('https://<YOUR_FRONTEND_API>/.well-known/jwks.json'))
const { payload, protectedHeader } = await jose.jwtVerify(token, JWKS)
jsonwebtoken doesn't work in middleware because it depends on crypto which is not available on edge functions.
Like you suggested, you can use jose. Make sure to use the key marked "PEM public key" from the Clerk dashboard (hidden under the advanced menu - it should start with -----BEGIN PUBLIC KEY-----).
Then, just pass it is to importSPKI as follows:
const { userId, getToken } = getAuth(request)
const token = await getToken({ template: "candor" })
const publicKey = await jose.importSPKI(process.env.CLERK_PUBLIC_KEY, "RS256")
const { payload } = await jose.jwtVerify(token, publicKey)
Related
I am trying to upload files to blob storage in azure from a react webapp but am having issues with the signature in the authorization header.
This is how the sasToken looks in my code
const sasToken = `sv=2020-08-04&ss=bfqt&srt=sco&sp=rwdlacupx&se=2021-09-22T00:41:33Z&st=2021-09-20T16:41:33Z&spr=https&sig=svP%2FecNOoteE%2**************%3D`;
const containerName = `containername`;
const storageAccountName = "acountname";
This is what it looks like the the GET and PUT requests of getBlobsInContainer and createBlobinContainer run.
sv=2020-08-04&ss=bfqt&srt=sco&sp=ghostery&se=2021-09-22T00:41:33Z&st=2021-09-20T16:41:33Z&spr=https&sig=svP/FecNOoteE/**************=
It's somehow overwriting the permission parameter in the token.
https://accountname.blob.core.windows.net/containername?SAS&comp=list&restype=container&_=1632199288178
The 3 functions I have to deal with it.
// return list of blobs in container to display
const getBlobsInContainer = async (containerClient) => {
const returnedBlobUrls = [];
// get list of blobs in container
// eslint-disable-next-line
for await (const blob of containerClient.listBlobsFlat()) {
// if image is public, just construct URL
returnedBlobUrls.push(
`https://${storageAccountName}.blob.core.windows.net/${containerName}/${blob.name}`
);
}
return returnedBlobUrls;
};
const createBlobInContainer = async (containerClient, file) => {
console.log(`initialising blobclient for ${file.name}`);
// create blobClient for container
const blobClient = containerClient.getBlockBlobClient(file.name);
console.log("blobclient generated");
// set mimetype as determined from browser with file upload control
const options = { blobHTTPHeaders: { blobContentType: file.type } };
// upload file
await blobClient.uploadBrowserData(file, options);
console.log("Adding Metadata");
await blobClient.setMetadata({UserName : 'Reynolds'});
};
const uploadFileToBlob = async (file) => {
if (!file) return [];
// get BlobService = notice `?` is pulled out of sasToken - if created in Azure portal
const blobService = new BlobServiceClient(
`https://${storageAccountName}.blob.core.windows.net?${sasToken}`
);
console.log(`blobservice: https://${storageAccountName}.blob.core.windows.net/?${sasToken}`);
// get Container - full public read access
const containerClient = blobService.getContainerClient(containerName);
// upload file
await createBlobInContainer(containerClient, file);
// // get list of blobs in container
return getBlobsInContainer(containerClient);
};
I'm basically trying to figure out why this is happening and how to prevent/avoid it. The code runs till the console.log(`blobservice: https://${storageAccountName}.blob.core.windows.net/?${sasToken}`); before breaking due to Error 403 from invalid signature.
Today I saw a number of tutorials on how to upload photos in react via the api.
I did everything, tried all the methods. But in the end I get stuck.
(During the whole explanation I will focus only on the features of the image upload)
In Models I have groups and variable -
[NotMapped]
public IFormFile ImageFile {get; set; }
In api I get
[Route ("Add")]
[HttpPost]
public void Post (Group group)
And I have in state-
const initialFieldValues = {
GroupName: '',
GroupAbout: '',
imageName: '',
imageSrc: defaultImageSrc,
imageFile: null
}
const [values, setValues] = useState (initialFieldValues)
When changing the image has a function-
const handleImg = (e) => {
if (e.target.files && e.target.files [0]) {
let imageFile = e.target.files [0];
const reader = new FileReader ();
reader.onload = x => {
setValues ({
... values,
imageFile,
imageSrc: x.target.result
})
}
reader.readAsDataURL (imageFile)
SetDisplayImg ("block");
}
else {
setValues ({
... values,
imageFile: null,
imageSrc: defaultImageSrc
})
}
};
And when submitting the form
const handleFormSubmit = e => {
e.preventDefault ()
const formData = new FormData ()
.append ('groupImage', values.imageFile)
addOrEdit (formData)
}
const addOrEdit = (formData) => {
axios.post ('api / groups / add', formData) .catch (error => {
console.log (error.response.data);
console.log (error.response.status);
console.log (error.response.headers);
});
}
In this code -makes error 415 (even regardless of uploading the image but, even if I put it only other variables that get stringed and work normally.)
If I add [FromForm] in the api it does not respond to me, i.e. it does not write me an error message nor does it reach the api (I checked in debugging)
If I change the axios to
const obj = {'groupImage': values.imageFile
}
axios.post ('api / groups / add', obj) .catch (error =>
I get an error message 400-
"The JSON value could not be converted to System.String. Path: $ .groupImage
And if I send the value from state
axios.post ('api / groups / add', values)
I get an error message System.NotSupportedException: Deserialization of interface types is not supported. Type 'Microsoft.AspNetCore.Http.IFormFile'. Path: $ .imageFile | LineNumber: 0 | BytePositionInLine: 6939781.
---> System.NotSupportedException: Deserialization of interface types is not supported. Type 'Microsoft.AspNetCore.Http.IFormFile'.
Anything I try to fix, it causes another error, I'm really at a loss.
Regards
>.append ('groupImage', values.imageFile)
Firstly, please make sure the key of your formdata object can match your model class property's name.
formData.append('imageName', values.imageName);
//...
//it should be imageFile, not groupImage
formData.append('imageFile', values.imageFile);
Besides, please apply the [FromForm] attribute to action parameter, like below.
public void Post([FromForm]Group group)
{
//...
Test Result
Usually a 415 means you aren't setting the right Content-Type header. Does the API you are trying to upload to mention acceptable types or encodings it expects?
I am building a Token authentication in React and have an issue. Below there are two lines of the code:
when I use proper code according to eslint:
const { token } = response.data.token;
it returns no error, but I cannot see the access_token, it is unidentified in local storage
when I use not proper code according to eslint:
const token=response.data.token;
then it shows me the actual code in local storage.
What is happening?
const { token } = response.data.token; is sugar for const token = response.data.token.token;, which is not what you want.
The following will be equivalent to your second snippet:
const { token } = response.data;
I am able to connect to the Coinbase public API without any problem from React Native. I'm trying to get API key authentication working. I think my code looks right but I'm getting a 401 error. I've done the same thing with Bittrex and it worked without issue.
I've mocked up a snippet of code to show the issue...
const key = 'XXXXXXXXXXXXXXXX'
const secret = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
const passphase = 'PASSPHASE'
const method = 'POST';
const timestamp = Date.now() / 1000;
const requestPath = '/orders';
const body = '';
const prehash = timestamp + method + requestPath + body;
const secretBase64 = Buffer(secret, 'base64');
const signed = CryptoJS.HmacSHA512(prehash, key);
const signedBase64 = new
Buffer(signed.toString()).toString('base64');
axios
.put(url, {
timeout: axiosTimeout,
headers: {
'CB-ACCESS-KEY': key,
'CB-ACCESS-SIGN': signedBase64,
'CB-ACCESS-TIMESTAMP': timestamp,
'CB-ACCESS-PASSPHRASE': passphrase
},
debug: true
})
.then(response => {
console.log(response);
})
.catch(error => {
console.log(error.message);
});
});
I created a read-only API key on Coinbase with the permission: wallet:orders:read
I use CryptoJS instead of crypto but that isn't an issue as it works with other exchanges like Bittrex. I converted the signed cypher to Base64 but I think the problem is probably on that line.
Console Log
In the example above I'm not using a valid key, secret or passphrase but using the actual key it doesn't work.
Can anyone see the problem?
I've been following this example on Coinbase:
https://docs.pro.coinbase.com/#api-key-permissions
I know there are some differences between "crypto" and "crypto-js".
I think it should look something like this:
const method = 'POST';
const timestamp = Date.now() / 1000;
const requestPath = url.replace(baseURL, '');
const body = JSON.stringify({
price: '1.0',
size: '1.0',
side: 'buy',
product_id: 'BTC-USD'
});
const prehash = timestamp + method + requestPath + body;
const keyBase64 = Buffer(secret, 'base64');
const signed = CryptoJS.HmacSHA512(prehash, secret).toString(CryptoJS.enc.Base64);
const headers = {
'CB-ACCESS-KEY': keyBase64,
'CB-ACCESS-SIGN': signed,
'CB-ACCESS-TIMESTAMP': timestamp,
'CB-ACCESS-PASSPHRASE': passphrase
}
The pre-hash ('what') in looks like this...
1554714678.037POST/orders{"price":"1.0","size":"1.0","side":"buy","product_id":"BTC-USD"}
The note on Coinbase says that the key needs to base64 decoded but they use the secret in their example. It also says the payload needs to be base64 encoded before adding to the header. I think I've done it all correctly but still getting the error message:
Error: Request failed with status code 401
When I created the API key on Coinbase I have it the correct permission. It provided the key and secret after generation. There was no mention of a passphase. I see in the documentation and in their code example they provide a passphase but what is this for and where should it be defined in Coinbase. Maybe that is the problem.
I have added this to codesandbox.io so it is easier to see what I am talking about... https://codesandbox.io/s/0q93n9vz20
I'm trying to use Google Cloud Print(GCP) API, but I can't make it works.
Maybe I've understood bad the workflow because is the first time I'm using the google api, please help me to understand how to make it works.
Initial considerations:
I'm trying to implement it in reactJS, but It is indifferent because the logic to make GCP works is independent of the technology. Then you also can help me understand the workflow.
What exactly I want:
To make my first test, I am looking to get all information about my printer.
What I did:
I created a project in: https://console.developers.google.com
Inside the project created, I created a credential:
create credentials -> OAuth client ID
And I chose Application type: Web, and also configure the restrictions to source and redirection to my localhost.
Manually in https://www.google.com/cloudprint, I added my printer, I made a test printing a PDF and was OK.
I created a project in reactJS to get the information of my printer I've added.
Component:
Explanation:
I'm using a component react-google-login to obtain easily the user accessToken: https://github.com/anthonyjgrove/react-google-login
This component only obtains the access token and save it in localStorage, in a variable called googleToken and it draws a button to call a function to obtain the information about the printer.
code:
import React, { Component } from 'react'
import GoogleLogin from 'react-google-login';
import { connect } from 'react-redux'
import { getPrinters } from '../actions/settings'
class Setting extends Component {
responseGoogle(response) {
const accessToken = response.accessToken
localStorage.setItem('googleToken', accessToken)
}
render() {
return (
<div>
<GoogleLogin
clientId="CLIENT_ID_REMOVED_INTENTIONALLY.apps.googleusercontent.com"
buttonText="Login"
onSuccess={this.responseGoogle}
onFailure={this.responseGoogle}
/>
<button
onClick = {() => {
this.props.getPrinters()
}}
>test printer</button>
</div>
)
}
}
const mapStateToProps = state => {
return {
state: state
}
}
const mapDispatchToProps = dispatch => {
return {
getPrinters() {
dispatch(getPrinters())
}
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Setting)
Action or Function to get information printer:
Explanation:
I'm passing the parameter printerid to get information about that printer.
In authorization, I'm using OAuth ... because in the documentation says that(second paragraph).: https://developers.google.com/cloud-print/docs/appInterfaces
The next two headers I wrote it because I tried solutions as:
Google Cloud Print API: User credentials required
Google Cloud Print User credentials required
code:
import axios from 'axios'
axios.defaults.headers.common['Authorization'] = 'OAuth ' + localStorage.getItem('googleToken')
axios.defaults.headers.common['scope'] = 'https://www.googleapis.com/auth/cloudprint'
axios.defaults.headers.common['X-CloudPrint-Proxy'] = 'printingTest'
const getPrinters = () => {
return () => {
return axios.get('https://www.google.com/cloudprint/printer'
, {
params: {
printeid: 'PRINTER_ID_REMOVED_INTENTIONALLY'
}
}
)
.then(response => {
console.log('response of google cloud print')
console.log(response)
})
}
}
export { getPrinters }
Error:
After all explained before, I got the next error:
User credentials required
Error 403
Note:
I'm using CORS plugin by recommendation of:
Chrome extensions for silent print?
because initially, I had cors error.
Any suggestion or recommendation would be very useful, thanks.
I've resolved my problem, my main problem about User Credential required were because I was using the incorrect access token and It was because I was getting the access token incorrectly.
I'm going to explain my whole solution because there are few examples of codes with this API.
Solutions:
The steps described were Ok until the fourth step where I used the external component react-google-login to trying to get the access token, instead I used googleapis module: Link Github googleapis
Also to avoid CORS problem(and not use CORS chrome plugin) I wrote the requests to Google API in server side.(NODEJS)
I had also a problem in the frontend when I tried to generate a popup to give permission for printer(problems about CORS), my solution was to use this very simple module for authentication: Link Github oauth-open
General scheme:
Explanation:
Knowing I have all data described in my question post(until the third step).
Authentication:
The next step in getting a URL and use it to the user can authenticate.
As I said before I used the module oauth-open in the frontend to generate the popup and only this module need the URL. To get the URL in the backend I used the endpoint /googleurl, where here I used the method generateAuthUrl of the module googleapis to generate the URL.
After that In the frontend, I got the authentication_code(that returned the module oauth-open), I send It to my endpoint /googletoken and here I process the authentication_code to generate access token, refresh token and expiration date with the method getToken of the module googleapis. Finally, these data are stored in the database.
Print:
For print, since the frontend, I send what data I need send to the printer. I used my endpoint /print
In the backend endpoint, my logic was the next:
Recover tokens and expiration date from database, with the expiration date check if the token has expired, and if It has already expired then gets another token and replace the old access token with the new one, replacing also with the new expiration date, to obtain this new data only is necessary call to method refreshAccessToken of module googleapis.Note: the refresh token never expires.
After having the access token updated, use it to send data to the printer with Google route(.../submit)
Code:
All the next codes are in only 1 file
Some data as validation, static variables, error handler, etc, has been removed to better understanding.
Route get URL authentication.
const express = require('express');
const google = require('googleapis');
const router = express.Router();
var OAuth2 = google.auth.OAuth2;
const redirect_url = 'http://localhost:3001/setting'; //Your redirect URL
var oauth2Client = new OAuth2(
'CLIENT ID', //Replace it with your client id
'CLIEND SECRET', //Replace it with your client secret
redirect_url
);
var url = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: 'https://www.googleapis.com/auth/cloudprint'
});
router.get('/googleurl', (req, res) => {
return res.status(200).send({
result: { googleURLToken: url }
});
});
To get tokens using the authentication code and save these in the database.
const Setting = require('../models/setting'); // My model(Mongoose)
router.post('/googletoken', (req, res) => {
oauth2Client.getToken(req.body.code, function (err, tokens) {
oauth2Client.credentials = tokens;
// If refresh token exits save it
// because the refresh token it returned only 1 time! IMPORTANT
if (tokens.hasOwnProperty('refresh_token')) {
let setting = new Setting();
setting.refreshTokenGoogle = tokens.refresh_token;
setting.expirationTokenGoogle = tokens.expiry_date;
setting.tokenGoogle = tokens.access_token;
setting.save()
.then((settingCreated) => {
return res.status(200).send({
message: 'OK'
});
})
}
});
});
To print
const axios = require('axios');
const moment = require('moment');
router.post('/print',async (req, res) => {
const tickeProperties = {
'version': '1.0',
'print': {
'vendor_ticket_item': [],
'color': { 'type': 'STANDARD_MONOCHROME' },
'copies': { 'copies': 1 }
}
};
const accessToken = await getTokenGoogleUpdated();
axios.get(
'https://www.google.com/cloudprint/submit',
{
params: {
printerid : printerID, // Replace by your printer ID
title: 'title printer',
ticket: tickeProperties,
content : 'print this text of example!!!',
contentType: 'text/plain'
},
headers: {
'Authorization': 'Bearer ' + accessToken
}
}
)
.then(response => {
return res.status(200).send({
result: response.data
});
})
}
);
async function getTokenGoogleUpdated() {
return await Setting.find({})
.then(async setting => {
const refreshTokenGoogle = setting[0].refreshTokenGoogle;
const expirationTokenGoogle = setting[0].expirationTokenGoogle;
const tokenGoogle = setting[0].tokenGoogle;
const dateToday = new Date();
// 1 minute forward to avoid exact time
const dateTodayPlus1Minute = moment(dateToday).add(1, 'm').toDate();
const dateExpiration = new Date(expirationTokenGoogle);
// Case date expiration, get new token
if (dateExpiration < dateTodayPlus1Minute) {
console.log('Updating access token');
oauth2Client.credentials['refresh_token'] = refreshTokenGoogle;
return await oauth2Client.refreshAccessToken( async function(err, tokens) {
// Save new token and new expiration
setting[0].expirationTokenGoogle = tokens.expiry_date;
setting[0].tokenGoogle = tokens.access_token;
await setting[0].save();
return tokens.access_token;
});
} else {
console.log('Using old access token');
return tokenGoogle;
}
})
.catch(err => {
console.log(err);
});
}
I hope It helps you if you want to use Google Cloud Print to not waste a lot of time as I did.
The important part there is a scope https://www.googleapis.com/auth/cloudprint which is not obvious and took one day for me to figure out.