I have built an iOS and an Android app. These apps use Cognito User Pools to allow public authenticated and unauthenticated access to API Gateway.
I am trying to prevent third-party apps from accessing this API. I only want apps that I have authorized to have API access. Is possible to limit access to APIG to only my apps?
Cognito provides the notion of an App Client ID and App Client Secret in the settings for the user pool. Is this the preferred mechanism to limit Cognito logins to whitelisted clients/apps? The docs provide little clarification as to the purpose of this configuration or a best practice for keeping these keys secure in the wild.
Are there alternative methods to achieve what I'm attempting? Is this goal even possible to accomplish? I believe an app could be reverse engineered for these keys or they could be discovered via a network sniffer.
I am a bit of a novice on API security so your insights are appreciated.
Have a look at Cognito Identity. Cognito Federated Identity Pools provide for both authenticated & unauthenticated access. This is going to be long, so please bear with me. (my code examples are using cloudformation in yaml or JS). I will assume that you have created your user pool & app client. You are going to need those to create an identity pool. I will also assume that your user pool allowed oauth flows is set to implicit grant and allowed oauth scope is openid. This is required to get the id_token which is used to create a federated identity.
create cognito identity pool using your cognito user pool as an authenticated provider. Sample CFN yaml for the same
AccIdenAdminPool:
Type: "AWS::Cognito::IdentityPool"
Properties:
IdentityPoolName: <identity pool name as input>
AllowUnauthenticatedIdentities: true
CognitoIdentityProviders:
- ClientId: <your app client id>
ProviderName: "cognito-idp.us-east-1.amazonaws.com/<your user pool id>"
ServerSideTokenCheck: true
Now attach your authenticated & unauthenticated roles to the identity pool you just created. Sample JS code -
module.exports.attachRole = (event, context, callback) => {
console.log(JSON.stringify(event)); // successful response
let params = {
IdentityPoolId: event.identityPoolId, /* required */
Roles: {
/* required */
'authenticated': <auth role arn>,
'unauthenticated': <unauth role arn>
},
};
cognitoidentity.setIdentityPoolRoles(params, function (err, data) {
if (err) {
console.log(err, err.stack);
}
else {
console.log("success"); // successful response
}
});
}
Change API gateway authentication from Cognito user pool authorizer to AWS_IAM. This is a must. If for some reason you cannot do this, you need to figure some other way out to have closed unauthenticated access to you APIs.
For authenticated access, use the id_token (received after successful login), identity pool id & user pool id to get CognitoIdentityCredentials. Sample code -
function getAccessToken(idToken, idenPoolId, userPool) {
let region = idenPoolId.split(":")[0];
let provider = "cognito-idp." + region + ".amazonaws.com/" + userPool;
let login = {};
login[provider] = idToken;
console.log(provider + ' || ' + idenPoolId);
// Add the User's Id Token to the Cognito credentials login map.
let credentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId: idenPoolId,
Logins: login
});
//call refresh method in order to authenticate user and get new temp credentials
credentials.get((error) => {
if (error) {
console.error(error);
} else {
console.log('Successfully logged!');
console.log('AKI:'+ credentials.accessKeyId);
console.log('AKS:'+ credentials.secretAccessKey);
console.log('token:' + credentials.sessionToken);
}
});
}
Use this access key,secret keyandtoken` to hit your APIs. It will have permissions based on your authenticated role you provisioned in step 2.
For unauthenticated access, login step will obviously be skipped but you can still generate temporary keys for access to your APIs. Sample code is very similar, with one key difference. Logins parameter is not required.
function getUnauthToken(idenPoolId) {
console.log(idenPoolId);
// Add the User's Id Token to the Cognito credentials login map.
let credentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId: idenPoolId,
});
credentials.get((error) => {
if (error) {
console.error(error);
} else {
console.log('Unauth AKI:'+ credentials.accessKeyId);
console.log('Unauth AKS:'+ credentials.secretAccessKey);
console.log('Unauth token:' + credentials.sessionToken);
}
});
}
This set of keys has permissions based on your unauthenticated role as provisioned in step 2.
Roles - this is how created roles & it's policy for my API gateway. Example in CFN yaml
AuthenticatedRole:
Type: "AWS::IAM::Role"
Properties:
RoleName: "AuthenticatedRole"
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
-
Effect: "Allow"
Action:
- "sts:AssumeRoleWithWebIdentity"
Principal:
Federated:
- "cognito-identity.amazonaws.com"
Condition:
StringEquals:
cognito-identity.amazonaws.com:aud: <your identity pool id>
ForAnyValue:StringLike:
cognito-identity.amazonaws.com:amr: authenticated
Path: "/"
AuthRolePolicy:
Type: "AWS::IAM::Policy"
Properties:
PolicyName: AuthRolePolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
-
Effect: "Allow"
Action: "execute-api:Invoke"
Resource:
- "arn:aws:execute-api:<region>:<account id>:<api id>/*/*/acc/*"]]
Roles:
-
Ref: AuthenticatedRole
UnauthRole:
Type: "AWS::IAM::Role"
Properties:
RoleName: UnauthRole
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
-
Effect: "Allow"
Action:
- "sts:AssumeRoleWithWebIdentity"
Principal:
Federated:
- "cognito-identity.amazonaws.com"
Condition:
StringEquals:
cognito-identity.amazonaws.com:aud: <your identity pool id>
Path: "/"
UnauthRolePolicy:
Type: "AWS::IAM::Policy"
Properties:
PolicyName: UnauthRolePolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
-
Effect: "Allow"
Action: "execute-api:Invoke"
Resource:
- "arn:aws:execute-api:<region>:<account id>:<api id>/*/GET"/acc/dept/12/*"]]
Roles:
-
Ref: UnauthRole
So based on above roles I have different access for my authenticated & unauthenticated users. Only caveat here is that your identity pool id must be a secret (i.e. not a good thing to expose in the browser).
Hope this helps.
Related
Strapi Version: 4.1.5
Operating System: Debian GNU/Linux 9
Database: PostgreSQL 13
Node Version: v14.16.0
NPM Version: 6.14.11
Yarn Version: v1.22.5
Hi everyone, I can’t seem to find consistent information on how to use permissions with a custom plugin in Strapi. I want to make an endpoint available to my front-end (Next.JS) application, but only when the front-end application has authenticated as a user and using the JWT that is returned from authenticating with Strapi. I keep getting a 401 returned.
Here’s what I’m doing:
I used this page to set up authentication in Strapi. I have a user created in Strapi, and from the front-end, I can authenticate and it returns a JWT token. When I set up collection types to only be accessible with the “authenticated” role, I can access those collection types in the api using this JWT token. So all of that works. The problem is that I can’t get this to work with my custom plugin, and I’m not sure why. I still get a 401 error instead.
Here’s how I set up the permissions:
Based on this page, I initially tried to leverage the isAuthenticated permission that the Users & Permissions plugin provides:
{
method: "GET",
path: "/progress",
handler: "memberProgress.getProgress",
config: {
policies: ['plugins::users-permissions.isAuthenticated']
},
},
Unfortunately, this did not work. The server raised an error, saying that this could not be found. So back on the document linked above, I decided to take the approach of creating my own gloabl permission. I created src/policies/is-authenticated.js with the following contents:
module.exports = (policyContext, config, { strapi }) => {
if (policyContext.state.user) { // if a session is open
// go to next policy or reach the controller's action
return true;
}
return false; // If you return nothing, Strapi considers you didn't want to block the request and will let it pass
};
Then, I modified my plugin’s route as follows:
{
method: "GET",
path: "/progress",
handler: "memberProgress.getProgress",
config: {
policies: ['global::is-authenticated']
},
},
This is all based on that document I linked to. Unfortunately, this still does not work. It seems to find the permission (server doesn’t raise an error about it), but when I try to access my plugin’s endpoint with the JWT token, I just get a 401 error.
Here is how I’m trying to access the endpoint on the front-end:
// VERIFIED, auth works and I get the expected jwt
const strapiAuth = await strapiApiAuth();
if ( strapiAuth && strapiAuth.hasOwnProperty("jwt") ) {
try {
const response = await axios.get(
`${process.env.STRAPI_BACKEND_URL}/member-progress/progress?year=2022&name=&pageSize=10&page=1`,
{
headers: {
Accept: "application/json",
Authorization: `Bearer ${strapiAuth.jwt}`
},
timeout: 500,
}
);
console.log(response);
} catch (error) {
// This is where I land with the 401 error
console.log(error);
}
}
Strapi check if you have a valid jwt by default with "authenticated" role, but you must mark the permission to your custom endpoint in "Settings→User & Permission Plugin→Roles" of admin panel also.
I am using a following CloudFormention Template to add cognito:preferred_role claim to my ID token.
IdentityPoolRoleAttachment:
Type: AWS::Cognito::IdentityPoolRoleAttachment
Properties:
IdentityPoolId: !Ref IdentityPool
Roles:
"authenticated": !GetAtt AuthenticatedRole.Arn
"unauthenticated": !GetAtt UnAuthenticatedRole.Arn
RoleMappings:
"userpool1":
IdentityProvider: !Join …
AmbiguousRoleResolution: Deny
Type: Token
But I recieved an ID token only contains standard claims on my client app.
I also noticed unauthenticated role is applyed to a user after login.
I changed AmbiguousRoleResolution to AuthenticatedRole but still no additional claims.
What I am missing to do?
I need to send this token to an api gateway to assume role base on cognito:roles or cognito:preferred_role claim to call dynamo sdk using authenticated role on lambda.
I'm trying to enable flow when some admin user by some admin client is able to create users and obtain their access tokens to be used for another clients.
I have KeyCloak setup with token exchange and fine grained authz enabled and configured clients. I'm able to login my admin user by REST api, then exchange token. But when I specify audience I got error.
This one returns token but I need token for another client/audience.
http -f POST https://my-keycloak-server.com/auth/admin/realms/my-realm/protocol/openid-connect/token grant_type=urn:ietf:params:oauth:grant-type:token-exchange requested_subject=1a147915-53fe-454d-906a-186fecfa6974 client_id=api-admin client_secret=23a4ecbe-a9e8-448c-b36a-a45fa1082e6e subject_token=eyJhbGeiOiJSUzI1NiIs......
This one is failing with error.
http -f POST https://my-keycloak-server.com/auth/admin/realms/my-realm/protocol/openid-connect/token grant_type=urn:ietf:params:oauth:grant-type:token-exchange requested_subject=1a147915-53fe-454d-906a-186fecfa6974 client_id=api-admin client_secret=23a4ecbe-a9e8-448c-b36a-a45fa1082e6e subject_token=eyJhbGeiOiJSUzI1NiIs...... audience=my-another-client
{
"error": "access_denied",
"error_description": "Client not allowed to exchange"
}
So I tried to setup fine grained auth for target audience client (enabled it in tab, then tried to add policy for my admin user to be able to exchange token) but when I want to add policy that will allow my admin user to perform token exchange I'm stuck on UI error.
When typing policy name I got 404 when Keycloak is looking for name colisions. Afaik 404 in this case shouldn't block form from posting because it is no name collision. Instead I got instantly redirected with error.
https://my-keycloak-server.com/auth/admin/realms/my-realm/clients/1bafa9a4-f7e2-422c-9188-58ea95db32ef/authz/resource-server/policy/search?name=some-name
In the end of the day I can't add any policy in Keycloak. All the time form validation is ending up with crash caused by 404 policy name not found.
I'm using dockerized keycloak 10.0.0
Any ideas?
I hacked it by live editing Angular JS UI script function that performs verification in line 2403.
this.checkNameAvailability = function (onSuccess) {
if (!$scope.policy.name || $scope.policy.name.trim().length == 0) {
return;
}
ResourceServerPolicy.search({
realm: $route.current.params.realm,
client: client.id,
name: $scope.policy.name
}, function(data) {
if (data && data.id && data.id != $scope.policy.id) {
Notifications.error("Name already in use by another policy or permission, please choose another one.");
} else {
onSuccess();
}
});
}
to
this.checkNameAvailability = function (onSuccess) {
onSuccess();
}
And that end up with successfuly added policy. Still looks like it's UI bug.
I have the following code in:
/oauth2/gARD:
get:
tags:
- RM API
summary: Fecthes as per user's request.
operationId: gARD
security:
- OAuth2: [read]
consumes:
- application/json
produces:
- application/json
parameters:
- in: header
name: token
description: Authorization
type: string
required: true
responses:
'200':
description: Successful
'500':
description: Returns error message
And I have the following in the components category below the above code:
components:
securitySchemes:
OAuth2:
type: oauth2
flow:
authorizationCode:
authorizationUrl: https://example.com/oauth/authorize
tokenUrl: https://example.com/oauth/token
scopes:
read: Grants read access
write: Grants write access
admin: Grants access to admin operations
Everything's referenced from this link. I am getting "Security requirements must match a security definition" error.
You are mixing OpenAPI 2.0 and 3.0 syntax. components.securitySchemes is OpenAPI 3.0 syntax, but your path definition uses 2.0 syntax.
Assuming you use OpenAPI 2.0, the security definition should look like:
securityDefinitions:
OAuth2:
type: oauth2
flow: accessCode
authorizationUrl: https://example.com/oauth/authorize
tokenUrl: https://example.com/oauth/token
scopes:
read: Grants read access
write: Grants write access
admin: Grants access to admin operations
I'm building a symfony REST Api in which I'm trying to get HWIOAuthBundle, FOSUserBundle and LexikJWTBundle working all together.
I followed this gist for the HWIOAuthBundle/FOSUserBundle integration.
Now I'm getting the facebook login form when hitting the /login route. But after submition I get this error :
[2/2] HttpTransportException: Error while sending HTTP request
[1/2] RequestException: Failed to connect to graph.facebook.com port 443: Bad access
INFO - Matched route "hwi_oauth_service_redirect".
CRITICAL -
Uncaught PHP Exception HWI\Bundle\OAuthBundle\OAuth\Exception\HttpTransportException:
"Error while sending HTTP request"
at C:\myProject\vendor\hwi\oauth-bundle\OAuth\ResourceOwner\AbstractResourceOwner.php
line 257
DEBUG -
Notified event "kernel.request" to listener
"Symfony\Component\EventDispatcher\Debug\WrappedListener::__invoke".
...
I'm now looking for help about this. Or any other way to get those bundles to work together.
config.yml :
hwi_oauth:
# list of names of the firewalls in which this bundle is active, this setting MUST be set
firewall_names: [auth]
http_client:
timeout: 10000
verify_peer: false
max_redirects: 1000
ignore_errors: false
fosub:
username_iterations: 300
properties:
# these properties will be used/redefined later in the custom FOSUBUserProvider service.
facebook: facebook_id
# an optional setting to configure a query string parameter which can be used to redirect
# the user after authentication, e.g. /connect/facebook?_destination=/my/destination will
# redirect the user to /my/destination after facebook authenticates them. If this is not
# set then the user will be redirected to the original resource that they requested, or
# the base address if no resource was requested. This is similar to the behaviour of
# [target_path_parameter for form login](http://symfony.com/doc/2.0/cookbook/security/form_login.html).
# target_path_parameter: _destination
# an optional setting to use the HTTP REFERER header to be used in case no
# previous URL was stored in the session (i.e. no resource was requested).
# This is similar to the behaviour of
# [using the referring URL for form login](http://symfony.com/doc/2.0/cookbook/security/form_login.html#using-the-referring-url).
# use_referer: true
# here you will add one (or more) configurations for resource owners
resource_owners:
facebook:
type: facebook
client_id: {id}
client_secret: {secret}
scope: ""
infos_url: "https://graph.facebook.com/me?fields=name,email,picture.type(square)"
options:
display: popup
security.yml :
firewalls:
auth:
pattern: ^/api/minisite/user/auth
anonymous: true
stateless: true
form_login:
check_path: /api/minisite/user/auth/login_check
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
username_parameter: username
password_parameter: password
require_previous_session: false
oauth:
resource_owners:
facebook: "/api/minisite/user/auth/facebook/login/check-facebook"
login_path: /api/minisite/user/auth/facebook/login
check_path: /api/minisite/user/auth/login_check
failure_path: /api/minisite/user/auth/facebook/login
oauth_user_provider:
#this is my custom user provider, created from FOSUBUserProvider - will manage the
#automatic user registration on your site, with data from the provider (facebook. google, etc.)
service: my_user_provider
logout: true
anonymous: true
access_control:
- { path: ^/api/minisite/user/auth, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/api/minisite, roles: IS_AUTHENTICATED_FULLY }
Configure this in your config.yml file.
hwi_oauth:
http_client:
verify_peer: false
Setting this allows you to turn off SSL verification.
I got the same problem using HWI on localhost. I don't know, but if it's your case, try to upload your work on a server. The reason of this issue is that your are using the port 80, but facebook need that you use the port 443 or use ipv6.
Hope this help