Extra information gets lost when updating user context - django-rest-framework

I've been fighting with this issue for days now and I just can't solve it. My app is built on React and Django Rest Framework. I'm authenticating users with JWT - when the user logs into the app, the React Auth context gets updated with some info about the tokens and I include some extra information in the context (namely the user email and some profile information) so that I have it easily accessible.
How I am doing this is by overwriting TokenObtainPairSerializer from simplejwt:
class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
#classmethod
def get_token(cls, user):
token = super().get_token(user)
# Add custom claims
token["email"] = user.email
token["information"] = Profile.objects.get(user=user).information
return token
On the frontend in my AuthContext.js:
const loginUser = async (email, password, firstLogin = false) => {
const response = await fetch(`${baseUrl}users/token/`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": csrfToken,
},
body: JSON.stringify({
email,
password,
}),
});
const data = await response.json();
if (response.status === 200) {
setAuthTokens(data);
setUser(jwt_decode(data.access));
localStorage.setItem("authTokens", JSON.stringify(data));
if (firstLogin) {
history.push("/profile");
} else {
history.push("/");
}
} else {
return response;
}
};
Up to this point it works perfectly fine and my ReactDevTools show me that the AuthContext has all the data:
Now to the issue - once the access token has expired, the next API call the user makes gets intercepted to update the token. I do this in my axiosInstance:
const useAxios = () => {
const { authTokens, setUser, setAuthTokens } = useContext(AuthContext);
const csrfToken = getCookie("csrftoken");
const axiosInstance = axios.create({
baseURL,
headers: {
Authorization: `Bearer ${authTokens?.access}`,
"X-CSRFToken": csrfToken,
"Content-Type": "application/json",
},
});
axiosInstance.interceptors.request.use(async (req) => {
const user = jwt_decode(authTokens.access);
const isExpired = dayjs.unix(user.exp).diff(dayjs()) < 1;
if (!isExpired) return req;
const response = await axios.post(`${baseURL}users/token/refresh/`, {
refresh: authTokens.refresh,
});
// need to add user info to context here
localStorage.setItem("authTokens", JSON.stringify(response.data));
setAuthTokens(response.data);
setUser(jwt_decode(response.data.access));
req.headers.Authorization = `Bearer ${response.data.access}`;
return req;
});
return axiosInstance;
};
export default useAxios;
But the extra information is not there. I tried to overwrite the TokenRefreshSerializer from jwt the same way as I did it with the TokenObtainPairSerializer but it just doesn't add the information
class MyTokenRefreshSerializer(TokenRefreshSerializer):
#classmethod
def get_token(cls, user):
token = super().get_token(user)
token["email"] = user.email
token["information"] = Profile.objects.get(user=user).information
print(token)
return token
It doesn't even print the token in my console but I have no clue what else I should try here.
Before anyone asks, yes I specified that the TokenRefreshView should use the custom serializer.
class MyTokenRefreshView(TokenRefreshView):
serializer_class = MyTokenRefreshSerializer
However, after a while of being logged into the application, the email and information key value pairs disappear from the context.
Any idea about how this can be solved will be much appreciated!

Related

Axios - Request header content-type was not present in the Access-Control-Allow-Headers list - ElasticSearch

I'm new to a lot of this technology, but I think I've diagnosed my issue and need some help. I've seen numerous posts on SO regarding this issue, but none have worked, though they have helped me diagnose issue.
I believe the issue is when I send the Header Content-Type w/ my pre-flight w/ Axios, it fails. This is possibly due to lower/case upper case? The error has lower case, but I tried both on the server without luck.
Basically, if I don't specify any header and Axios uses json as content-type, it works, but as soon as I specify Content-Type my pre-flight fails (even though I think post would work..).
Here is the elasticsearch.yml
cluster.name: "docker-cluster"
network.host: 0.0.0.0
http.cors.enabled : true
http.cors.allow-origin: "*"
http.cors.allow-methods: OPTIONS,HEAD,GET,POST,PUT,DELETE
http.cors.allow-headers: X-Requested-With,X-Auth-Token,Content-Type,Content-Length
#http.cors.allow-credentials: true
Here is my JS that I'm testing BTW w/ an Office Add-In solution in Visual Studio 2017 which I think is using IE as a browser.
Main Func:
var URL = "https://elasticsearch:9200/users/_search"
const data = {
"query": {
"match": {
"name": "freesoftwareservers"
}
}
};
Do_Axios('get', URL, data, null, false)
Do_Axios('post', URL, data, null, false)
Do_Axios:
async function Do_Axios(method, URL, data, headers, withCredentials) {
return axios({
method: method,
url: URL,
withCredentials: withCredentials,
//contentType: 'application/json', // does nothing
//data: JSON.stringify(data), //Causes urlformencoded which is wrong
data: data, //caues type to be json and I get error
headers: {
//"Content-Type": "application/json"
},
})
.then(function (response) {
console.log("Axios " + method + " response:");
console.log(response)
return response;
})
.catch(function (error) {
console.log(error);
});
}
Note: I can get/post if I comment out //data but then the post doesn't run my query. If I uncomment data then Axios uses urlformencoded but that doesn't work.
For now, I've been able to search API via urlformencoded queries, but I'd like to fix my ability to POST correctly to resolve future errors. I'm unsure if issue should be pointed to Axios or Elasticsearch if I open a request.
Well, I finally figured it out. I wonder how many of the other posts I read have similar issues... anyway, the issue was w/ my NGinX proxy server. No better way to learn about CORS then to setup an API and make CORS requests via IE! Without the below, I was still able to post w/ POSTMAN to the same URL which hit my nginx server, but the call from Axios/IE/JS Evironment failed.
I found these snippets and this was the magic that needed added to my "regular" configuration:
proxy_pass_header Access-Control-Allow-Origin;
proxy_pass_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
add_header Access-Control-Allow-Headers 'X-Requested-With, Content-Type';
add_header Access-Control-Allow-Credentials true;
https://gist.github.com/sahilsk/b16cb51387847e6c3329
Here is my code as it stands, cleaned up but generic atm:
Note: I pass axios because I can't figure out how to get my Webpack to transform/polyfill my funcs in seperate js files. But I can declare axios in the main func and pass it and then I can move my funcs into separate files as needed for organization. There is likely a better way to do without passing axios and configuring webpack
Main Func:
var username = "freesoftwareservers"
var ipv4 = "192.168.1.255"
var showhelp = "false"
await Do_AddUserToES(axios,username, ipv4, showhelp)
Get_UserFromES(axios,username)
var index = "users"
var query = {
query: {
match: {
"username": username
}
}
};
Get_PostQueryToES(axios,query, index)
Funcs:
function Do_Axios(axios, method, URL, data, headers, withCredentials) {
return axios({
method: method,
url: URL,
withCredentials: withCredentials,
data: data,
headers: headers,
})
.then(function (response) {
console.log("Axios " + method + " response:");
console.log(response)
return response;
})
.catch(function (error) {
console.log(error);
});
}
function Get_ESURL(Bool_Search, Bool_Doc, Bool_Update, Opt_Index, Opt_IndexKey) {
var ESUrl = "https://elasticsearch:9200"
var ESSearch = "/_search"
var ESDoc = "/_doc"
var ESUpdate = "/_update"
var ReturnURL = ESUrl
if (Opt_Index != undefined) { ReturnURL = ReturnURL + "/" + Opt_Index }
if (Bool_Search == true) { ReturnURL = ReturnURL + ESSearch }
if (Bool_Doc == true) { ReturnURL = ReturnURL + ESDoc }
if (Bool_Update == true) { ReturnURL = ReturnURL + ESUpdate }
if (Opt_IndexKey != undefined) { ReturnURL = ReturnURL + "/" + Opt_IndexKey }
console.log("ReturnURL:" + ReturnURL)
return ReturnURL;
}
function Do_AddUserToES(axios, username, ipv4, showhelp) {
var adduser = {
"username": username,
"ipv4": ipv4,
"showhelp": showhelp
};
var URL = Get_ESURL(false, true, false, "users", username)
return Do_Axios(axios, 'post', URL, adduser, null, false);
}
function Get_UserFromES(axios, username) {
var URL = Get_ESURL(false, true, false, "users", username)
return Do_Axios(axios, 'get', URL, null, null, false);
}
function Get_PostQueryToES(axios, query, index) {
var URL = Get_ESURL(true, false, false, index)
return Do_Axios(axios, 'post', URL, query, null, false);
}

Authorize GMail API with JWT

I'm trying to send an email through the gmail API from a Node.js application. I had this working, following the documentation and using the node-mailer package. However, I noticed that when we change our organizations password, the connection is no longer good (which makes sense). I'm therefore trying to authorize with a JWT instead.
The JWT is correctly generated and posted to https://oauth2.googleapis.com/token. This request then returns an access_token.
When it comes time to write and send the email, I tried to simply adapt the code that was previously working (at the time with a client_secret, client_id and redirect_uris):
const gmail = google.gmail({ version: 'v1', auth: access_token });
gmail.users.messages.send(
{
userId: 'email',
resource: {
raw: encodedMessage
}
},
(err, result) => {
if (err) {
return console.log('NODEMAILER - The API returned: ' + err);
}
console.log(
'NODEMAILER Sending email reply from server: ' + result.data
);
}
);
The API keeps returning Error: Login Required.
Does anyone know how to solve this?
EDIT
I've modified my code and autehntication to add the client_id and client_secret:
const oAuth2Client = new google.auth.OAuth2(
credentials.gmail.client_id,
credentials.gmail.client_secret,
credentials.gmail.redirect_uris[0]
);
oAuth2Client.credentials = {
access_token: access_token
};
const gmail = google.gmail({ version: 'v1', auth: oAuth2Client });
gmail.users.messages.send(
{
userId: 'email',
resource: {
raw: encodedMessage
}
},
(err, result) => {
if (err) {
return console.log('NODEMAILER - The API returned: ' + err);
}
console.log(
'NODEMAILER Sending email reply from server: ' + result.data
);
}
);
But now the error is even less precise: Error: Bad Request
Here's the final authorization code that worked for me:
var credentials = require('../../credentials');
const privKey = credentials.gmail.priv_key.private_key;
var jwtParams = {
iss: credentials.gmail.priv_key.client_email,
scope: 'https://www.googleapis.com/auth/gmail.send',
aud: 'https://oauth2.googleapis.com/token',
exp: Math.floor(new Date().getTime() / 1000 + 120),
iat: Math.floor(new Date().getTime() / 1000),
sub: [INSERT EMAIL THAT WILL BE SENDING (not the service email, the one that has granted delegated access to the service account)]
};
var gmail_token = jwt.sign(jwtParams, privKey, {
algorithm: 'RS256'
});
var params = {
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
assertion: gmail_token
};
var params_string = querystring.stringify(params);
axios({
method: 'post',
url: 'https://oauth2.googleapis.com/token',
data: params_string,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}).then(response => {
let mail = new mailComposer({
to: [ARRAY OF RECIPIENTS],
text: [MESSAGE CONTENT],
subject: subject,
textEncoding: 'base64'
});
mail.compile().build((err, msg) => {
if (err) {
return console.log('Error compiling mail: ' + err);
}
const encodedMessage = Buffer.from(msg)
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
sendMail(encodedMessage, response.data.access_token, credentials);
});
});
So that code segment above uses a private key to create a JSON Web Token (JWT), where: iss is the service account to be used, scope is the endpoint of the gmail API being accessed (this must be preauthorized), aud is the google API oAuth2 endpoint, exp is the expiration time, iat is the time created and sub is the email the service account is acting for.
The token is then signed and a POST request is made to the Google oAuth2 endpoint. On success, I use the mailComposer component of NodeMailer to build the email, with an array of recipients, a message, a subject and an encoding. That message is then encoded.
And here's my sendMail() function:
const oAuth2Client = new google.auth.OAuth2(
credentials.gmail.client_id,
credentials.gmail.client_secret,
credentials.gmail.redirect_uris[0]
);
oAuth2Client.credentials = {
access_token: access_token
};
const gmail = google.gmail({ version: 'v1', auth: oAuth2Client });
gmail.users.messages.send(
{
userId: 'me',
resource: {
raw: encodedMessage
}
},
(err, result) => {
if (err) {
return console.log('NODEMAILER - The API returned: ' + err);
}
console.log(
'NODEMAILER Sending email reply from server: ' + result.data
);
}
);
In this function, I am creating a new googleapis OAuth2 object using the credentials of the service account (here stored in an external file for added security). I then pass in the access_token (generated in the auth script with the JWT). The message is then sent.
Pay attention to the userId: 'me' in the sendMail() function, this was critical for me.
This is the way I was able to only use googleapis package instead of axios + googleapis with your service account. You will need domain wide authority for this account with the scope used below associated with it. Follow this to do that https://support.google.com/a/answer/162106?hl=en
You can also use the mailComposer example up above to create the email. keys is the service_credentials.json file you get when making this service account
const { google } = require('googleapis');
const scope = ["https://www.googleapis.com/auth/gmail.send"];
const client = new google.auth.JWT({
email: keys.client_email,
key: keys.private_key,
scopes: scope,
subject: "emailToSendFrom#something.com",
});
await client.authorize();
const gmail = google.gmail({ version: 'v1', auth: client});
const subject = '🤘 Hello 🤘';
const utf8Subject = `=?utf-8?B?${Buffer.from(subject).toString('base64')}?=`;
const messageParts = [
'From: Someone <emailToSendFrom#something.com>',//same email as above
'To: Someone <whoever#whoever.com>',
'Content-Type: text/html; charset=utf-8',
'MIME-Version: 1.0',
`Subject: ${utf8Subject}`,
'',
'This is a message just to say hello.',
'So... <b>Hello!</b> 🤘❤️😎',
];
const message = messageParts.join('\n');
// The body needs to be base64url encoded.
const encodedMessage = Buffer.from(message)
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
const res = await gmail.users.messages.send({
userId: 'me',
requestBody: {
raw: encodedMessage,
},
});
console.log(res.data);

Downloading an attachment from Teams across a Bot Nodejs

I'm facing the following error when I try to get the attachment in Microsoft Teams with Bot Builder v4:
{"message":"Authorization has been denied for this request."}
everything works fine with the version 3, as far as I know in Teams is necessary a token in order get the binary array of the file.
In the v3 I'm able to get the jwt token in this way:
connector.getAccessToken.bind(connector)
and then I use it in the header of the GET request =>
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/octet-stream'
}
In the v4:
context.adapter.getUserToken(step.context, CONNECTION_SETTING_NAME);
is there another way to get a valid token in the v4?
To get the token just call the prompt again. You can find the auth sample here for node. The sample happens to use a waterfall dialog, which may not be needed in your case
let prompt = await step.prompt(OAUTH_PROMPT);
If the token is valid and not expired you can get the token like below, if the token is not valid or the user does not have a token they will be prompted to log in. Otherwise the token will be in the prompt result.
var tokenResponse = prompt.result;
if (tokenResponse != null) {
await step.context.sendActivity(`Here is your token: ${ tokenResponse.token }`);
}
These comments from the sample should help explain
// Call the prompt again because we need the token. The reasons for this are:
// 1. If the user is already logged in we do not need to store the token locally in the bot and worry
// about refreshing it. We can always just call the prompt again to get the token.
// 2. We never know how long it will take a user to respond. By the time the
// user responds the token may have expired. The user would then be prompted to login again.
//
// There is no reason to store the token locally in the bot because we can always just call
// the OAuth prompt to get the token or get a new token if needed.
these are the steps
/**
* WaterfallDialogStep to process the user's picture.
* #param {WaterfallStepContext} step WaterfallStepContext
*/
async processPhotoStep(step) {
await this.writeLogInTheStorage('Start downloading picture....');
await this.handleIncomingAttachment(step);
return await step.endDialog();
};
/**
* responds to the user with information about the saved attachment or an error.
* #param {Object} turnContext
*/
async handleIncomingAttachment(step) {
// Prepare Promises to download each attachment and then execute each Promise.
const attachment = step.context.activity.attachments[0];
const tokenIsRequired = await this.checkRequiresToken(step.context);
const dc = await this.dialogs.createContext(step.context);
const token = await dc.beginDialog(LOGIN_PROMPT); //await step.context.adapter.getUserToken(step.context, CONNECTION_SETTING_NAME);
let file = undefined;
if (tokenIsRequired) {
file = await this.downloadAttachment(token.result.token, attachment.contentUrl);
}
else {
file = await requestX(attachment.contentUrl);
}
await OAuthHelpers.postPhoto(step.context, token.result, file);
}
async downloadAttachment(token, url) {
const p = new Promise((resolve, reject) => {
request({
url: url,
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/octet-stream'
}
}, async function (err, response, body) {
const result = body
if (err) {
console.log(err);
//await this.writeLogInTheStorage('err 1 : ' + err);
reject(err);
} else if (result.error) {
console.log(result.error);
//await this.writeLogInTheStorage('err 2 : ' + err);
reject(result.error.message);
} else {
// The value of the body will be an array.
console.log(result);
//await this.writeLogInTheStorage('success : ' + result);
resolve(result);
}
});
});
return p;
}

JWT is not set in header

I'm following this tutorial, currently I can log in and out with a user but when a user logs in the JWT token isn't send with the header request (I think) so I get a 401 after the router.navigate. When I reload the page I can use the token and everything works.
In my login.component.ts I have this login function:
login() {
this.loading = true;
this.authenticationService.login(this.model.username, this.model.password)
.subscribe(result => {
if (result === true) {
// login successful
this.router.navigate(['home']);
} else {
// login failed
this.error = 'Username or password is incorrect';
this.loading = false;
}
}, error => {
this.loading = false;
this.error = error;
});
}
This calls the login function in the authentication.service.ts:
login(username: string, password: string): Observable<boolean> {
return this.http.post(this.authUrl, JSON.stringify({username: username, password: password}), {headers: this.headers})
.map((response: Response) => {
// login successful if there's a jwt token in the response
const token = response.json() && response.json().token;
if (token) {
// store username and jwt token in local storage to keep user logged in between page refreshes
localStorage.setItem('currentUser', JSON.stringify({ username: username, token: token }));
// return true to indicate successful login
alert('Success');
return true;
} else {
// return false to indicate failed login
alert('Fail');
return false;
}
}).catch((error: any) => Observable.throw(error.json().error || 'Server error'));
}
If the login is successful the user is routed to /home:
this.router.navigate(['home']);
In the home.component.ts I have a getAll function that returns all movies in the database:
getAll() {
this._dataService
.getAll<Movie[]>()
.subscribe((data: any[]) => this.movies = data,
error => () => {
'something went wrong';
},
() => {
console.log(this.movies);
});
}
This function is called on the ngOnInit:
ngOnInit(): void {
this.getAll();
}
In my app.service.ts I have the get function:
public getAll<T>(): Observable<T[]> {
if (this.authenticationService.getToken()) {
console.log(this.authenticationService.getToken());
console.log(this.headers);
return this.http.get<T[]>('/api/movies/all', {headers: this.headers});
}
}
But when I log in I get this error after being routed to the home page:
GET http://localhost:4200/api/movies/all 401 (Unauthorized)
The problem (I think) is that when I get routed to the home page the header is missing the token. But as you can see from the console log the token is available in app.service.ts.
When I reload the page I do have the token set in the header and everything works:
Any ideas on how to expose the token to the header after the redirect?
//EDIT
For some reason I do get the JWT token when I set the header directly in the function:
return this.http.get<T[]>('/api/movies/all', {headers: new HttpHeaders().set('Authorization', 'Bearer ' + this.authenticationService.getToken())});
Instead of calling it like this:
headers = new HttpHeaders().set('Authorization', 'Bearer ' + this.authenticationService.getToken());
return this.http.get('/api/movies/' + id, {headers: this.headers});

Ajax and Django render_to_response (How to render to response inside success function)

Currently I am trying to implement a login validation system. I am using ajax so that users can get a response without being redirected to another page. My ajax function sends email and password that user has inputted, and get message in callback function, which can be in three types: email, password, or the actual HttpResponse object. But I have no idea how to render the given http response object using ajax and jquery. Is location.href an option? I am pasting the code below.
In javascript:
function loginSubmit(email, password) {
var d= "email=" + email + "&password=" + password;
$.ajax({
url: "/login",
type: "POST",
dataType: "text",
data: d,
success: function(m) {
if (m == "email") {
$("#emailMessage").html("There is no account associated with this email address.");
$("#emailError").show();
$("#emailError").fadeOut(5000, function() {});
} else if (m == "password") {
$("#emailMessage").html("There is no account associated with this email address.");
$("#emailError").show();
$("#emailError").fadeOut(5000, function() {});
} else {
}
}
});
}
in view function:
def login(request):
json = request.POST
e = json['email']
p = json['password']
u = User.objects.filter(email=e)
if (len(u)):
up = User.objects.filter(email=e, password=p)
if (len(up)):
return render_to_response('profile.html', context_instance=RequestContext(request))
else:
data = "password"
c = RequestContext(request, {'result':data})
t = Template("{{result}}")
datatype=u"application/javascript"
return HttpResponse(t.render(c), datatype)
else:
data = "email"
c = RequestContext(request, {'result':data})
t = Template("{{result}}")
datatype=u"application/javascript"
return HttpResponse(t.render(c), datatype)
p.s. Currently I am using a dummy template and HttpResponse to send data to the ajax success callback function. Is there a more efficient way to accomplish this (send back json data)? I will wait for your replies guys!
from django.contrib.auth import authenticate, login as auth_login
def login(request):
# Use authentication framework to check user's credentials
# http://djangosnippets.org/snippets/1001/ for auth backend
user = authenticate(
email = request.POST['email'],
password = request.POST['password'], )
if user is not None:
# Use Auth framework to login user
auth_login(request, user)
return render_to_response('profile.html',
context_instance=RequestContext(request))
else:
# Return Access Denied
# Never return bad email/bad password. This is information leakage
# and helps hackers determine who uses your platform and their emails.
return HttpResponse("Failed: Bad username or password", status=403)
function loginSubmit(email, password) {
$.ajax({
url: "/login",
type: "POST",
data: {email:email, password:password},
success: function(data) {
var returned_html = $(data);
$("#target_profile_area").clear().append(returned_html);
},
error: function(jqXHR) {
if (jqXHR.statusCode == 403) {
$("#loginMessage").text("Your login details are incorrect");
} else {
$("#loginMessage").text("Error Contacting Server");
}
$("#loginError").show();
$("#loginError").fadeOut(5000, function() {});
}
});
}

Resources