I want to control access to a public AWS API, meaning only my website and mobile app can consume this API, otherwise the API will reject the requests.
My approach is to secure the API using AWS Cognito with OAuth2 scopes, see more details here. The website won't need user registration, so I use cognito just to secure the API.
So far, I'm able to go to Hosted UI under App client settings in AWS Cognito. Click the link there to login, and get a "code": https://example.com/?code=f27b2de0-1111-1111-1111-11111111. And then follow How to use the code returned from Cognito to get AWS credentials? to get id_token. And finally send a HTTP request with header key=Authorization and value=<id_token>. This works for me. I see that the API rejects requests when no valid token, and returns expected results when valid token is present.
However, I have some questions:
The token expires in 30 days(I configured it to be so), and my website is a reactJS app, how can the website refresh the token? My website should not care any login here, it just need to send a valid ID Token as the Authorization header to access the API. Users should not see any login page. Should I use Cognito SDK amazon-cognito-identity-js in my react app to fetch the ID token?
Do I need setup callback url here? All I need is ensure that the API will reject the request if token is missing or invalid, I don't know what's the purpose of having callback url in this case.
Feel free to point out any other mistakes I made here.
Sample code
Here are my CDK code to setup API + Cognito.
import * as CDK from "aws-cdk-lib";
import * as CertificateManager from "aws-cdk-lib/aws-certificatemanager";
import * as Route53 from "aws-cdk-lib/aws-route53";
import * as Route53Targets from "aws-cdk-lib/aws-route53-targets";
import * as ApiGateway from "aws-cdk-lib/aws-apigateway";
import * as ELBv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";
import { Construct } from "constructs";
import { StageInfo } from "../config/stage-config";
import * as Cognito from "aws-cdk-lib/aws-cognito";
export interface ApigatewayStackProps extends CDK.StackProps {
readonly packageName: string;
readonly stageInfo: StageInfo;
}
export class ApigatewayStack extends CDK.Stack {
// Prefix for CDK constrcut ID
private readonly constructIdPrefix: string;
private readonly pandaApiCognitoUserPool: Cognito.UserPool;
private readonly domainCertificate: CertificateManager.Certificate;
private readonly apiAuthorizer: ApiGateway.CfnAuthorizer;
private readonly pandaApi: ApiGateway.RestApi;
constructor(scope: Construct, id: string, props: ApigatewayStackProps) {
super(scope, id, props);
this.constructIdPrefix = `${props.packageName}-${props.stageInfo.stageName}`;
const hostedZone: Route53.IHostedZone = Route53.HostedZone.fromLookup(
this,
`${this.constructIdPrefix}-HostedZoneLookup`,
{
domainName: props.stageInfo.domainName,
}
);
this.domainCertificate = new CertificateManager.Certificate(
this,
`${this.constructIdPrefix}-pandaApiCertificate`,
{
domainName: props.stageInfo.domainName,
validation:
CertificateManager.CertificateValidation.fromDns(hostedZone),
}
);
this.pandaApi = new ApiGateway.RestApi(
this,
`${this.constructIdPrefix}-pandaApi`,
{
description: "The centralized API for panda.com",
domainName: {
domainName: props.stageInfo.domainName,
certificate: this.domainCertificate,
//mappingKey: props.pipelineStageInfo.stageName
},
defaultCorsPreflightOptions: {
allowOrigins: ApiGateway.Cors.ALL_ORIGINS,
allowMethods: [...ApiGateway.Cors.DEFAULT_HEADERS],
},
}
);
new Route53.ARecord(this, "AliasRecord", {
zone: hostedZone,
target: Route53.RecordTarget.fromAlias(
new Route53Targets.ApiGateway(this.pandaApi)
),
// or - route53.RecordTarget.fromAlias(new alias.ApiGatewayDomain(domainName)),
});
this.pandaApiCognitoUserPool = new Cognito.UserPool(this, "UserPool", {
userPoolName: `pandaApiUserPool`,
selfSignUpEnabled: false,
});
this.apiAuthorizer = new ApiGateway.CfnAuthorizer(
this,
`${this.constructIdPrefix}-pandaApiAuthorizer`,
{
name: "pandaApiAuthorizer",
type: ApiGateway.AuthorizationType.COGNITO,
identitySource: "method.request.header.Authorization",
restApiId: this.pandaApi.restApiId,
providerArns: [this.pandaApiCognitoUserPool.userPoolArn],
}
);
this.addCognitoAuthentication(props);
}
private addCognitoAuthentication(props: ApigatewayStackProps) {
this.pandaApiCognitoUserPool.addDomain("DomainName", {
cognitoDomain: {
domainPrefix: `panda-api-user-pool-${props.stageInfo.stageName.toLocaleLowerCase()}`,
},
});
this.pandaApiCognitoUserPool.addClient(
`${this.constructIdPrefix}-pandaApiUserPoolClient`,
{
userPoolClientName: `pandaApiUserPoolClient`,
generateSecret: true,
oAuth: {
flows: {
// It's highly recommend to use only the Authorization code grant flow.
// https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-app-idp-settings.html
authorizationCodeGrant: true,
},
scopes: [Cognito.OAuthScope.OPENID],
//callbackUrls: [props.stageInfo.domainName + '/callback']
},
authFlows: {
userPassword: true,
},
refreshTokenValidity: CDK.Duration.days(30),
}
);
}
}
Cognito is for logging your users in and providing you an access token and id token as a result. Your users must log in. If they don't log in then you can't use cognito to secure you API. Further if you don't require log in, then anyone in the world can call your API.
There is no way to ensure only your website and your mobile app can talk to your API, this isn't how APIs work. APIs are available to the whole world, they are all public, you can't block requests to them, unless your users individually authenticate.
The reason for this is that any one can spoof your website or mobile application, and because of that no security mechanisms exist that would enable restricting access.
So either your API is world-writable/readable or you require your users to log in. The good news is there are tons of ways to easily do this, now that logs of AuthN providers support webauthn/fido2/passkeys out of the box.
you can probably google around for a list of providers that offer exactly what you need, but a starter list is something like this one on how to pick the best auth provider.
I have an API with JWT Authentication enabled. I am trying to login a user and then update the user data. In JWT Auth, after login the server provides a token which is needed to update the user data. Now I can login a user and the server is giving me a token. I converted the token into LoginResponseModel.
These are the response model:
class LoginResponseModel {
String token;
String userNicename;
LoginResponseModel({this.token, this.userNicename});
factory LoginResponseModel.fromJson(Map<String, dynamic> json) {
return LoginResponseModel(
token : json['token'],
userNicename : json['user_nicename'],
);}
}
Although I can insert the token into model, I cannot access the token from my APIService class creating a LoginResponseModel class instance.
My approach of accessing the token is something like this:
LoginResponseModel loginResponseMode;
String token = loginResponseMode.token
var dio = Dio();
dio.options.headers["Authorization"] = "Bearer $token";
// here is my network call code
But here i got null value of token. Would you please help me out how to access only the token from LoginResponseModel ? Thank you.
Use case:
From inside a EventListenerProvider on an event I want to make an authenticated REST call to one of our keycloak secured service. For this I need a token.
First I just test printing the token to check whether it is succeeded.
public void onEvent(final Event event) {
Keycloak k = Keycloak.getInstance("http://localhost:8080/auth", "myrealm", "myemail#gmail.com", "password", "myclient");
AccessTokenResponse t = k.tokenManager().getAccessToken();
logger.info(t.getSessionState());
logger.info(t.getToken());
}
Unfortunatly both the session_state and token is NULL.
All the data are correct, the url,the realm..etc. Otherwise we would know about that. Keycloak doesnt log anything just silently returns null.
On the top of that I can use the above code from anywhere else and it works! I can use it from a plain java main() method and still works. Getting token by hand via postman also works.
What is wrong with the Keycloak Provider? How can I get an accesstoken for a particular user?
You can use the following example to create a AccessToken:
public String getAccessToken(UserModel userModel, KeycloakSession keycloakSession) {
KeycloakContext keycloakContext = keycloakSession.getContext();
AccessToken token = new AccessToken();
token.subject(userModel.getId());
token.issuer(Urls.realmIssuer(keycloakContext.getUri().getBaseUri(), keycloakContext.getRealm().getName()));
token.issuedNow();
token.expiration((int) (token.getIat() + 60L)); //Lifetime of 60 seconds
KeyWrapper key = keycloakSession.keys().getActiveKey(keycloakContext.getRealm(), KeyUse.SIG, "RS256");
return new JWSBuilder().kid(key.getKid()).type("JWT").jsonContent(token).sign(new AsymmetricSignatureSignerContext(key));
}
Note that you also need to specify <module name="org.keycloak.keycloak-services"/> in your jboss-deployment-structure.
I am using the react-native-facebook-login package to log users in. Currently the flow is working well and after the user enters their details, I successfully see an object returned with their information.
When I try and create an account in Firebase with signInWithCredential, I receive the following error message:
signInWithCredential failed: First argument "credential" must be a valid
I can't seem to find a breakdown of how that credential needs to be passed - is it a string, an object, an array etc. Is it just the token or do I need to pass other details (i.e. the provider)?
The credentials object I am currently getting back has:
permission: Array
token: String
tokenExpirationDate: String
userId: String
Any help would be much appreciated - thanks!
Feeling pretty pleased - finally cracked the nut.
They key bit is the token needs to be changed first before being a relevant credential. See code below:
onLogin={function(data){
let token = firebase.auth.FacebookAuthProvider.credential(data.credentials.token);
firebase.auth().signInWithCredential(token)
.then((user) => {
console.log(user)
}).catch((err) => {
console.error('User signin error', err);
});
}}
to answer your question, based on the documentation of firebase:
where GoogleAuthProvider could be any of your setup / supported auth providers
// Build Firebase credential with the Google ID token.
var credential = firebase.auth.GoogleAuthProvider.credential(id_token);
// Sign in with credential from the Google user.
firebase.auth().signInWithCredential(credential).catch(function(error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
// The email of the user's account used.
var email = error.email;
// The firebase.auth.AuthCredential type that was used.
var credential = error.credential;
// ...
});
On a side note, as you are using react-native and firebase, did you already try react-native-firestack? makes a lot of things easier.
My application exposes a REST API for services and uses SpringSecurity to manage login at the private services.
With custom signup and login I don't have any kind of problem, but now I try to implement login/signup with Facebook or Twitter, and I don't know how to do this.
Has anyone had the same problem and solved it?
I tried to use a custom password "very long" for every Facebook and Twitter account but that didn't work.
UPDATE
I try your solution, but get an error. This is my code
public UserDetails loadUserByUsername(String mail) throws UsernameNotFoundException {
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
if (ConstantPWCabinet.SOCIAL_LOGIN_FACEBOOK.equalsIgnoreCase(attr.getRequest().getParameter(ConstantPWCabinet.LOGIN_TYPE))) {
User facebookInfo = dao.getFacebookInfo(new FacebookTemplate(attr.getRequest().getParameter(ConstantPWCabinet.FACEBOOK_TOKEN)));
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
authorities.add(new SimpleGrantedAuthority(Role.ROLE_USER_FACEBOOK.toString()));
org.springframework.security.core.userdetails.User user = new org.springframework.security.core.userdetails.User(facebookInfo.getEmail(), null, authorities);
Authentication auth = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
return user;
}
logger.debug("Mail di accesso: " + mail);
User user = dao.getUserSelectedMail(mail);
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
String role = user.getRole().toString();
if (StringUtils.isEmpty(role))
authorities.add(new SimpleGrantedAuthority(Role.ROLE_USER.toString()));
else
authorities.add(new SimpleGrantedAuthority(role));
return new org.springframework.security.core.userdetails.User(user.getEmail(), user.getPassword(), authorities);
}
But i get and "Bad credential" and no get login.
You have an AuthenticationFilter that listens to url j_spring_security_check
Filter creates an authentication object and sends to Authentication Provider.
AuthenticationProvider calls UserDetailsService to load user by username and authenticate the user.
Filter then checks the authentication object returned by the provider and sends request to success/failure handler.
When you do it through social medium, your user is authenticated by an external source, so you do not need to authenticate user at your end.
You can simple do
// Authenticate the user
UserDetails user = userDetailsService.loadUserByUsername(username);
Authentication auth = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
This will authenticate the user without password.