Django DRF + Allauth: OAuth2Error: Error retrieving access token on production build - reactjs

We are integrating DRF (dj_rest_auth) and allauth with the frontend application based on React. Recently, the social login was added to handle login through LinkedIn, Facebook, Google and GitHub. Everything was working good on localhost with each of the providers. After the staging deployment, I updated the secrets and social applications for a new domain. Generating the URL for social login works fine, the user gets redirected to the provider login page and allowed access to login to our application, but after being redirected back to the frontend page responsible for logging in - it results in an error: (example for LinkedIn, happens for all of the providers)
allauth.socialaccount.providers.oauth2.client.OAuth2Error:
Error retrieving access token:
b'{"error":"invalid_redirect_uri","error_description":"Unable to retrieve access token: appid/redirect uri/code verifier does not match authorization code. Or authorization code expired. Or external member binding exists"}'
Our flow is:
go to frontend page -> click on provider's icon ->
redirect to {BACKEND_URL}/rest-auth/linkedin/url/ to make it a POST request (user submits the form) ->
login on provider's page ->
go back to our frontend page {frontend}/social-auth?source=linkedin&code={the code we are sending to rest-auth/$provider$ endpoint}&state={state}->
confirm the code & show the profile completion page
The adapter definition (same for every provider):
class LinkedInLogin(SocialLoginView):
adapter_class = LinkedInOAuth2Adapter
client_class = OAuth2Client
#property
def callback_url(self):
return self.request.build_absolute_uri(reverse('linkedin_oauth2_callback'))
Callback definition:
def linkedin_callback(request):
params = urllib.parse.urlencode(request.GET)
return redirect(f'{settings.HTTP_PROTOCOL}://{settings.FRONTEND_HOST}/social-auth?source=linkedin&{params}')
URLs:
path('rest-auth/linkedin/', LinkedInLogin.as_view(), name='linkedin_oauth2_callback'),
path('rest-auth/linkedin/callback/', linkedin_callback, name='linkedin_oauth2_callback'),
path('rest-auth/linkedin/url/', linkedin_views.oauth2_login),
Frontend call to send the access_token/code:
const handleSocialLogin = () => {
postSocialAuth({
code: decodeURIComponent(codeOrAccessToken),
provider: provider
}).then(response => {
if (!response.error) return history.push(`/complete-profile?source=${provider}`);
NotificationManager.error(
`There was an error while trying to log you in via ${provider}`,
"Error",
3000
);
return history.push("/login");
}).catch(_error => {
NotificationManager.error(
`There was an error while trying to log you in via ${provider}`,
"Error",
3000
);
return history.push("/login");
});
}
Mutation:
const postSocialUserAuth = builder => builder.mutation({
query: (data) => {
const payload = {
code: data?.code,
};
return {
url: `${API_BASE_URL}/rest-auth/${data?.provider}/`,
method: 'POST',
body: payload,
}
}
Callback URLs and client credentials are set for the staging environment both in our admin panel (Django) and provider's panel (i.e. developers.linkedin.com)
Again - everything from this setup is working ok in the local environment.
IMPORTANT
We are using two different domains for the backend and frontend - frontend has a different domain than a backend

The solution was to completely change the callback URL generation
For anyone looking for a solution in the future:
class LinkedInLogin(SocialLoginView):
adapter_class = CustomAdapterLinkedin
client_class = OAuth2Client
#property
def callback_url(self):
callback_url = reverse('linkedin_oauth2_callback')
site = Site.objects.get_current()
return f"{settings.HTTP_PROTOCOL}://{site}{callback_url}"
Custom adapter:
class CustomAdapterLinkedin(LinkedInOAuth2Adapter):
def get_callback_url(self, request, app):
callback_url = reverse(provider_id + "_callback")
site = Site.objects.get_current()
return f"{settings.HTTP_PROTOCOL}://{site}{callback_url}"
It is important to change your routes therefore for URL generation:
path('rest-auth/linkedin/url/', OAuth2LoginView.adapter_view(CustomAdapterLinkedin))
I am leaving this open since I think this is not expected behaviour.

Related

Amplify: 'No Cognito Federated Identity pool provided' with federation login

I'm using Amplify's React UI kit, and I'm trying to get my social logins working with Cognito.
At the moment, I'm getting No Cognito Federated Identity pool provided
Here's the code for the buttons:
<AmplifyAuthenticator federated={{
googleClientId:
'*id*',
facebookAppId: '*id*'
}} usernameAlias="email">
<AmplifySignUp headerText="Create Account" slot="sign-up"/>
<AmplifySignIn slot="sign-in">
<div slot="federated-buttons">
<AmplifyGoogleButton onClick={() => Auth.federatedSignIn()}/>
<AmplifyFacebookButton onClick={() => Auth.federatedSignIn()}/>
</div>
</AmplifySignIn>
</AmplifyAuthenticator>
I've tried making an identity pool, and filling in the authentication providers for Cognito, Google and Facebook and still getting the same error.
For the URI's in both the providers, I've included the domain address as a authorised javascript origin, and for the redirect, I added oauth2/idpresponse to the end of the domain.
This is working within the Amplify hosted UI, just not with my React solution.
On Cognito, my redirect is my domain /token. As I wait for Amplify to set the cookies before redirecting the user.
token.tsx
export default function TokenSetter() {
const router = useRouter();
useAuthRedirect(() => {
// We are not using the router here, since the query object will be empty
// during prerendering if the page is statically optimized.
// So the router's location would return no search the first time.
const redirectUriAfterSignIn =
extractFirst(queryString.parse(window.location.search).to || "") || "/";
router.replace(redirectUriAfterSignIn);
});
return <p>loading..</p>;
}
Here's my Amplify.configure()
Amplify.configure({
Auth: {
region: process.env.USER_POOL_REGION,
userPoolId: process.env.USER_POOL_ID,
userPoolWebClientId: process.env.USER_POOL_CLIENT_ID,
IdentityPoolId: process.env.IDENTITY_POOL_ID,
oauth: {
domain: process.env.IDP_DOMAIN,
scope: ["email", "openid"],
// Where users get sent after logging in.
// This has to be set to be the full URL of the /token page.
redirectSignIn: process.env.REDIRECT_SIGN_IN,
// Where users are sent after they sign out.
redirectSignOut: process.env.REDIRECT_SIGN_OUT,
responseType: "token",
},
},
});
In order to call an user pool federated IDP directly from your app you need to pass the provider option to Auth.federatedSignIn:
Auth.federatedSignIn({
provider: provider,
})
The options are defined in an enum.
export enum CognitoHostedUIIdentityProvider {
Cognito = 'COGNITO',
Google = 'Google',
Facebook = 'Facebook',
Amazon = 'LoginWithAmazon',
Apple = 'SignInWithApple',
}
Not exhaustive unfortunately. If you have a custom OIDC or SAML idp you use the provider name or id.
For Facebook the call looks like so:
Auth.federatedSignIn({
provider: 'Facebook',
})
Integrated with a button:
const FacebookSignInButton = () => (
<AmplifyButton
onClick={()=>Auth.federatedSignIn({provider: 'Facebook'})}>
Sign in with Facebook
</AmplifyButton>
)

HTTP method’s error: 401 (Unauthorized) on React & Django(DRF)

I completed manipulating authentication with token by referring this article, and then I’m trying to create a crud function such creating post, displaying posts, etc… . However, I have an error when I fetched the url which displays posts(IE, fetching url I defined as “index” method on views.py of app for auth manipulation), I have 401 error even though I can access by using url of the backend without any error even on terminal.
I found some config codes which are related to authentication and permission for manipulation of authentication with token on settings.py causes this error, since when I delete these codes, the crud function works. But obviously authentication function no longer works (index method on views.py retrieve only token, another informations are filled blank) by this solution.
//fetch method on frontend
try{
const res = await fetch(`${base_url}/accounts/current_user/`,{
method:'GET',
headers:{
Authorization:`JWT ${localStorage.getItem('token')}`
}
})
const data = await res.json();
setUsername(data.username);
console.log(data)
}catch(err){console.log(err)};
//fetch posts on frontend
const getProblems = async() =>{
const res = await fetch(base_url+'/problems/index');
const data = await res.json();
setProblems(data);
}
//views.py on app for auth manipulation
#api_view(['GET'])
def get_current_user(request):
serializer = GetFullUserSerializer(request.user)
print(serializer.data)
return Response(serializer.data)
//settings.py(related to auth, cors):
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
)
}
CORS_ORIGIN_WHITELIST = (
'http://localhost:3000',
)
CORS_ALLOW_CREDENTIALS = True
JWT_AUTH = {
'JWT_RESPONSE_PAYLOAD_HANDLER':
'new_sns.utils.custom_jwt_response_handler',
}
I have another files such serializer.py, urls.py,but they are absolutely same as the article I extracted.
I guess I misunderstand something around configuration. I would like to hear some of suggestions. Please let me know if you think if there may be problems on another files which I didn't attach on here.
Thanks.
Try changing JWT ${localStorage.getItem('token')} to Authorization:`Bearer ${localStorage.getItem('token')}.
In case this won't work, try djangorestframework-simplejwt - package recommended on DRF's docs.
JSON Web Token is a fairly new standard which can be used for
token-based authentication. Unlike the built-in TokenAuthentication
scheme, JWT Authentication doesn't need to use a database to validate
a token. A package for JWT authentication is
djangorestframework-simplejwt which provides some features as well as
a pluggable token blacklist app.
django-api-logger and axios-jwt may also come handy.

Getting Error while uploading file to S3 with Federated Identity using aws-amplify in React

While the user with Facebook federated Identity trying to upload Image, I'm getting an error: AWSS3Provider - error uploading Error: "Request failed with status code 403"
Status Code: 403 Forbidden
Noticed that URL in request, while user authenticated with Federated Identity (Facebook), looks:
Request URL: https://my-gallery-api-dev-photorepos3bucket-XXXX.s3.us-east-2.amazonaws.com/private/undefined/1587639369473-image.jpg?x-id=PutObject
The folder where the uploaded image will be placed is 'undefined' instead of being a valid user identity like for users authenticated with from AWS UserPool, see:
Request URL: https://my-gallery-api-dev-photorepos3bucket-XXXX.s3.us-east-2.amazonaws.com/private/us-east-2%3Aa2991437-264a-4652-a239-XXXXXXXXXXXX/1587636945392-image.jpg?x-id=PutObject
For Authentication and upload I'am using React aws dependency "aws-amplify": "^3.0.8"
Facebook Authentication (Facebook Button):
async handleResponse(data) {
console.log("FB Response data:", data);
const { userID, accessToken: token, expiresIn } = data;
const expires_at = expiresIn * 1000 + new Date().getTime();
const user = { userID };
this.setState({ isLoading: true });
console.log("User:", user);
try {
const response = await Auth.federatedSignIn(
"facebook",
{ token, expires_at },
user
);
this.setState({ isLoading: false });
console.log("federatedSignIn Response:", response);
this.props.onLogin(response);
} catch (e) {
this.setState({ isLoading: false })
console.log("federatedSignIn Exception:", e);
alert(e.message);
this.handleError(e);
}
}
Uploading:
import { Storage } from "aws-amplify";
export async function s3Upload(file) {
const filename = `${Date.now()}-${file.name}`;
const stored = await Storage.vault.put(filename, file, {
contentType: file.type
});
return stored.key;
}
const attachment = this.file
? await s3Upload(this.file)
: null;
I'm understand that rejection by S3 with 403, because of the IAM role, I have for authenticated users:
# IAM role used for authenticated users
CognitoAuthRole:
Type: AWS::IAM::Role
Properties:
Path: /
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: 'Allow'
Principal:
Federated: 'cognito-identity.amazonaws.com'
Action:
- 'sts:AssumeRoleWithWebIdentity'
Condition:
StringEquals:
'cognito-identity.amazonaws.com:aud':
Ref: CognitoIdentityPool
'ForAnyValue:StringLike':
'cognito-identity.amazonaws.com:amr': authenticated
Policies:
- PolicyName: 'CognitoAuthorizedPolicy'
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: 'Allow'
Action:
- 'mobileanalytics:PutEvents'
- 'cognito-sync:*'
- 'cognito-identity:*'
Resource: '*'
# Allow users to invoke our API
- Effect: 'Allow'
Action:
- 'execute-api:Invoke'
Resource:
Fn::Join:
- ''
-
- 'arn:aws:execute-api:'
- Ref: AWS::Region
- ':'
- Ref: AWS::AccountId
- ':'
- Ref: ApiGatewayRestApi
- '/*'
# Allow users to upload attachments to their
# folder inside our S3 bucket
- Effect: 'Allow'
Action:
- 's3:*'
Resource:
- Fn::Join:
- ''
-
- Fn::GetAtt: [PhotoRepoS3Bucket, Arn]
- '/private/**${cognito-identity.amazonaws.com:sub}/***'
- Fn::Join:
- ''
-
- Fn::GetAtt: [PhotoRepoS3Bucket, Arn]
- '/private/**${cognito-identity.amazonaws.com:sub}**'
It works fine for users registered in AWS User Pool (Email, Password), but for federated users, there is no record in AWS User Pool only in Federated Identities, so there will be no cognito-identity.amazonaws.com:sub found for those users and directory 'undefined' not falling in role allowance for user identified with Federated Identity.
Please advise:
1. Where/how to fix this 'undefined' in URL?
2. Also, I would like, probably, to replace thouse Id's in upload URL to genereted user Id's from user database I'm going to add in near future. How to fix IAM Role to use custom Id's?
I stumbled with the same problem when doing Serverless Stack tutorial
This error arises when you do the Extra Credit > React > Facebook Login with Cognito using AWS Amplify, as you have notice uploading a file fails if you're authenticated with Facebook.
The error comes up when sending a PUT to:
https://<bucket>.s3.amazonaws.com/private/<identity-id>/<file>
...the <identity-id> is undefined so the PUT fails.
You can track down the source of this undefinition if you log what you get when running the login commands. For example, when you login using your email and password, if you do:
await Auth.signIn(fields.email, fields.password);
const currCreds = await Auth.currentCredentials();
console.log('currCreds', currCreds);
...you can see that identityId is set correctly.
On the other hand when you login with Facebook through Auth.federatedSignIn if you log the response you don't get identityId. Note: In the case you've previously logged in using email and password, it will remain the same, so this misconfiguration will also make uploading fail.
The workaround I've used is adding a simple lambda which returns the identityId for the logged in user, so once the user logs in with facebook, we ask for it and we can send the PUT to the correct url using AWS.S3().putObject
In the case you want to try this out, take into account that you should host your React app in https as Facebook doesn't allow http domains. You can set this adding HTTPS=true to your React .env file.
You can check my repos as example:
API
Frontend

How do I manage an access token, when storing in local storage is not an option?

I have a ReactJS app running in browser, which needs access to my backend laravel-passport API server. So, I am in control of all code on both client and server side, and can change it as I please.
In my react app, the user logs in with their username and password, and if this is successful, the app recieves a personal access token which grants access to the users data. If I store this token in local storage, the app can now access this users data by appending the token to outgoing requests.
But I do not want to save the access token in local storage, since this is not secure. How do I do this?
Here is what I have tried:
In the laravel passport documentation, there is a guide on how to automatically store the access token in a cookie. I believe this requires the app to be on the same origin, but I cannot get this to work. When testing locally, I run the app on localhost:4000, but the API is run on my-app.localhost. Could this be a reason why laravel passport does not make a cookie with the token, although they technically both have origin localhost?
OAuth has a page on where to store tokens. I tried the three options for "If backend is present", but they seem to focus on how the authorization flow rather than how to specifically store the token.
Here's the relevant parts of my code (of course, feel free to ask for more if needed):
From my react app:
const tokenData = await axios.post(this.props.backendUrl + '/api/loginToken', { email: 'myEmail', password: 'myPassword' })
console.log('token data: ', tokenData)
const personalAccessToken = tokenData.data.success.token;
var config = {
headers: {
'Authorization': "Bearer " + personalAccessToken
};
const user = await axios.get(this.props.backendUrl + '/api/user', config);
From the controller class ApiController:
public function loginToken()
{
if (Auth::attempt(['email' => request('email'), 'password' => request('password')])) {
$user = Auth::user();
$success['token'] = $user->createToken('MyApp')->accessToken;
return response()->json(['success' => $success], 200);
} else {
return response()->json(['error' => 'Unauthorised'], 401);
}
}
and the loginToken function is called from the /api/loginToken route.
Expected and actual results:
Ideally, I would love to have the token saved in a cookie like in the passport documentation, so I don't even have to attach the token to outgoing requests from the react app, but I'm not sure that this is even possible. Perhaps with third party cookies?
Else, I'd just like to find some way to store the token securely (for example in a cookie?), and then append it to outgoing calls from the react app.

Authentication with oidc-client.js and Identityserver4 in a React frontend

Lately I'm trying to set-up authentication using IdentityServer4 with a React client. I followed the Adding a JavaScript client tutorial (partly) of the IdentityServer documentation: https://media.readthedocs.org/pdf/identityserver4/release/identityserver4.pdf also using the Quickstart7_JavaScriptClient file.
The downside is that I'm using React as my front-end and my knowledge of React is not good enough to implement the same functionality used in the tutorial using React.
Nevertheless, I start reading up and tried to get started with it anyway. My IdentityServer project and API are set-up and seem to be working correctly (also tested with other clients).
I started by adding the oidc-client.js to my Visual Code project. Next I created a page which get's rendered at the start (named it Authentication.js) and this is the place where the Login, Call API and Logout buttons are included. This page (Authentication.js) looks as follows:
import React, { Component } from 'react';
import {login, logout, api, log} from '../../testoidc'
import {Route, Link} from 'react-router';
export default class Authentication extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<div>
<button id="login" onClick={() => {login()}}>Login</button>
<button id="api" onClick={() => {api()}}>Call API</button>
<button id="logout" onClick={() => {logout()}}>Logout</button>
<pre id="results"></pre>
</div>
<div>
<Route exact path="/callback" render={() => {window.location.href="callback.html"}} />
{/* {<Route path='/callback' component={callback}>callback</Route>} */}
</div>
</div>
);
}
}
In the testoidc.js file (which get's imported above) I added all the oidc functions which are used (app.js in the example projects). The route part should make the callback.html available, I have left that file as is (which is probably wrong).
The testoidc.js file contains the functions as follow:
import Oidc from 'oidc-client'
export function log() {
document.getElementById('results').innerText = '';
Array.prototype.forEach.call(arguments, function (msg) {
if (msg instanceof Error) {
msg = "Error: " + msg.message;
}
else if (typeof msg !== 'string') {
msg = JSON.stringify(msg, null, 2);
}
document.getElementById('results').innerHTML += msg + '\r\n';
});
}
var config = {
authority: "http://localhost:5000",
client_id: "js",
redirect_uri: "http://localhost:3000/callback.html",
response_type: "id_token token",
scope:"openid profile api1",
post_logout_redirect_uri : "http://localhost:3000/index.html",
};
var mgr = new Oidc.UserManager(config);
mgr.getUser().then(function (user) {
if (user) {
log("User logged in", user.profile);
}
else {
log("User not logged in");
}
});
export function login() {
mgr.signinRedirect();
}
export function api() {
mgr.getUser().then(function (user) {
var url = "http://localhost:5001/identity";
var xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.onload = function () {
log(xhr.status, JSON.parse(xhr.responseText));
}
xhr.setRequestHeader("Authorization", "Bearer " + user.access_token);
xhr.send();
});
}
export function logout() {
mgr.signoutRedirect();
}
There are multiple things going wrong. When I click the login button, I get redirected to the login page of the identityServer (which is good). When I log in with valid credentials I'm getting redirected to my React app: http://localhost:3000/callback.html#id_token=Token
This client in the Identity project is defined as follows:
new Client
{
ClientId = "js",
ClientName = "JavaScript Client",
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser = true,
// where to redirect to after login
RedirectUris = { "http://localhost:3000/callback.html" },
// where to redirect to after logout
PostLogoutRedirectUris = { "http://localhost:3000/index.html" },
AllowedCorsOrigins = { "http://localhost:3000" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
}
}
Though, it seems the callback function is never called, it just stays on the callback url with a very long token behind it..
Also the getUser function keeps displaying 'User not logged in' after logging in and the Call API button keeps saying that there is no token. So obviously things are not working correctly. I just don't know on which points it goes wrong.
When inspecting I can see there is a token generated in the local storage:
Also when I click the logout button, I get redirected to the logout page of the Identity Host, but when I click logout there I don't get redirected to my client.
My questions are:
Am I on the right track implementing the oidc-client in combination with IdentityServer4?
Am I using the correct libraries or does react require different libraries than the oidc-client.js one.
Is there any tutorial where a react front-end is used in combination with IdentityServer4 and the oidc-client (without redux), I couldn't find any.
How / where to add the callback.html, should it be rewritten?
Could someone point me in the right direction, there are most likely more things going wrong here but at the moment I am just stuck in where to even begin.
IdentityServer4 is just a backend implementation of OIDC; so, all you need to do is implement the flow in the client using the given APIs. I don't know what oidc-client.js file is but it is most likely doing the same thing that you could have implemented yourself. The flow itself is very simple:
React app prepares the request and redirects the user to the Auth server with client_id and redirect_uri (and state, nonce)
IdentityServer checks if the client_id and redirect_uri match.
If the user is not logged in, show a login box
If a consent form is necessary (similar to when you login via Facebook/Google in some apps), show the necessary interactions
If user is authenticated and authorized, redirect the page to the redirect_uri with new parameters. In your case, you the URL will look like this: https://example.com/cb#access_token=...&id_token=...&stuff-like-nonce-and-state
Now, the React app needs to parse the URL, access the values, and store the token somewhere to be used in future requests:
Easiest way to achieve the logic is to first set a route in the router that resolves into a component that will do the logic. This component can be "invisible." It doesn't even need to render anything. You can set the route like this:
<Route path="/cb" component={AuthorizeCallback} />
Then, implement OIDC client logic in AuthorizeCallback component. In the component, you just need to parse the URL. You can use location.hash to access #access_token=...&id_token=...&stuff-like-nonce-and-state part of the URL. You can use URLSearchParams or a 3rd party library like qs. Then, just store the value in somewhere (sessionStorage, localStorage, and if possible, cookies). Anything else you do is just implementation details. For example, in one of my apps, in order to remember the active page that user was on in the app, I store the value in sessionStorage and then use the value from that storage in AuthorizeCallback to redirect the user to the proper page. So, Auth server redirects to "/cb" that resolves to AuthorizeCallback and this component redirects to the desired location (or "/" if no location was set) based on where the user is.
Also, remember that if the Authorization server's session cookie is not expired, you will not need to relogin if the token is expired or deleted. This is useful if the token is expired but it can be problematic when you log out. That's why when you log out, you need to send a request to Authorization server to delete / expire the token immediately before deleting the token from your storage.

Resources