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

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

Related

How do I avoid React Native GoogleSignIn by sending to google a password violation

I used #react-native-google-signin/google-signin": "^8.0.0" to create a google sign in button into my app.
When I used it, google recognised it as not trusted app, so send my an email to advice of a violation, and now every time I use a password saved on my google account to login on any site or application, he gives me a message telling me to change all of my passwords. I solved to remove the message, by ignoring for every passwords, almost 200 :/. But it's just temporary solution, cause if I do login again to my app it will happen again. How can I say to google that it is an app in developing, is there any mode to activate? Here's my code:
GoogleSignin.configure({
scopes: ['https://www.googleapis.com/auth/drive.readonly'], // what API you want to access on behalf of the user, default is email and profile
webClientId: '15299853035-njb79hdij6h1svo22drigurca1qb4djb.apps.googleusercontent.com', // client ID of type WEB for your server (needed to verify user ID and offline access)
offlineAccess: true, // if you want to access Google API on behalf of the user FROM YOUR SERVER
// hostedDomain: '', // specifies a hosted domain restriction
// forceCodeForRefreshToken: true, // [Android] related to `serverAuthCode`, read the docs link below *.
// accountName: '', // [Android] specifies an account name on the device that should be used
iosClientId: '15299853035-siujgcjtol0lfja83n7p6fk55cq6jinn.apps.googleusercontent.com', // [iOS] if you want to specify the client ID of type iOS (otherwise, it is taken from GoogleService-Info.plist)
// googleServicePlistPath: '', // [iOS] if you renamed your GoogleService-Info file, new name here, e.g. GoogleService-Info-Staging
// openIdRealm: '', // [iOS] The OpenID2 realm of the home web server. This allows Google to include the user's OpenID Identifier in the OpenID Connect ID token.
// profileImageSize: 120, // [iOS] The desired height (and width) of the profile image. Defaults to 120px
});
try {
await GoogleSignin.hasPlayServices();
const { idToken } = await (await GoogleSignin.signIn());
const googleCredential = await GoogleAuthProvider.credential(idToken);
await signInWithCredential(authApp, googleCredential)
.then(async(userCredential) => {
if(!authApp.currentUser.emailVerified)
{
sendEmailVerification(authApp.currentUser)
.then(() => {
// Email verification sent!
// ...
})
.catch((error)=>{
setLoading(false)
console.log(error)
})
}
......

MS Graph API's subscription create to OnlineMessages fails

I am attempting to create a subscription to the resource /communications/onlineMeetings/?$filter=JoinWebUrl eq '{JoinWebUrl}' with the node ms graph client.
To do this, I have:
Two tenants, one with an active MS Teams license (Office 365 developer), whereas the other tenant houses my client app, which is a multi-tenant app.
Added the required scope to the client app (App level scope: OnlineMeetings.Read.All)
Given admin consent to the client app from the MS Teams tenant. The screenshot below shows the client app scope details in the MS Teams tenant.
Initialized the MSAL auth library as follows in the client app:
const authApp = new ConfidentialClientApplication({
auth: {
clientId: 'app-client-id',
clientSecret: 'app-client-secret',
authority: `https://login.microsoftonline.com/${tenantId}`,
},
});
Gotten an accessToken via the call:
const authContext = await authApp.acquireTokenByClientCredential({
authority: `https://login.microsoftonline.com/${tenantId}`,
scopes: ['https://graph.microsoft.com/.default'],
skipCache: true,
});
const accessToken = authContext.accessToken;
Initialized the MS Graph client as follows:
const client = MSClient.init({
debugLogging: true,
authProvider: (done) => {
done(null, accessToken);
},
});
Created a subscription successfully for the: CallRecords.Read.All scope (which correctly sends call record notifications to the defined webhook) with the following call:
const subscription = await client
.api('/subscriptions')
.version('beta')
.create({
changeType: 'created,updated',
notificationUrl: `https://my-ngrok-url`,
resource: '/communications/callrecords',
clientState: 'some-state',
expirationDateTime: 'date-time',
});
Attempted to create a subscription for the OnlineMeetings.Read.All scope with the following call:
const subscription = await client
.api('/subscriptions')
.version('beta')
.create({
resource: `/communications/onlineMeetings/?$filter=JoinWebUrl eq '{JoinWebUrl}'`,
changeType: 'created,updated',
notificationUrl: `https://my-ngrok-url`,
clientState: 'some-state',
expirationDateTime: 'date-time',
includeResourceData: true,
encryptionCertificate: 'serialized-cert',
encryptionCertificateId: 'cert-id',
});
This results in the error message:
GraphError: Operation: Create; Exception: [Status Code: Forbidden;
Reason: The meeting tenant does not match the token tenant.]
I am unsure what is causing this and and how to debug it further. Any help would be much appreciated.
My understanding of the joinWebUrl in the resource definition was incorrect -- the joinWebUrl is a placeholder for an actual online-meeting's URL (rather than a catch-all for all created meetings).
What I was hoping to achieve however is (currently) not possible, which is creating a subscription on a particular user's join/leave events to any online-meeting.

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

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.

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>
)

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.

Resources