I have a class that wraps http calls (get, post, delete).
What I need to achieve is a way to check every api response to be able to force a logout if, for example, a token is expired.
Let's say I'm logged in my app and my token expires; I then try to list my posts with a call like this:
var results = api.get('http://api.site.com/user/123/posts')
get(String endpoint) async {
return http.get(endpoint,
headers: headers(),
).then((response) {
var result = json.decode(response.body);
if (result['force_logout'] == true) {
UserServices.logout(); // delete secure storage data and other stuff
Navigator.of(context).pushNamedAndRemoveUntil('/login', (Route<dynamic> route) => false);
}
return result;
});
}
Inside my api get method I would like to be able to perform the user logout and navigate to the login screen, but how can I do it without having a context?
Related
I'm currently developing a flutter app that sends some http requests to an external service, but actually, I keep some API keys in the app, and I want to secure them using cloud functions. How can I create a function that executes something like this? This is a Stripe request that I actually make from the app.
Future<Customer?> createCustomer(String userId) async {
final String url = 'https://api.stripe.com/v1/customers';
Map<String,String> headers = {
'Authorization': 'Bearer <API_KEY_HERE>',
'Content-Type': 'application/x-www-form-urlencoded'
};
var response = await _client.post(
Uri.parse(url),
headers: headers,
body: {'name': "test", "metadata[userId]": userId},
);
final decodedResult = json.decode(response.body);
log.info(decodedResult);
if (response.statusCode == 200) {
try {
final customer = Customer.fromMap(decodedResult);
currentCustomer = customer;
return customer;
} catch (e) {
log.error(e.toString());
return null;
}
} else {
return null;
}
}
You cloud very well, from a Cloud Function, call the Stripe REST API with, for example, the axios library.
But since we use the Admin SDK for Node.js to write Cloud Functions for Firebase, I would suggest to use the Stripe Node.js API (This link points to the customers.create method).
You can do that from any type of Cloud Function, e.g. a Callable one that you call from your app or a background triggered one, e.g. when a new customer doc is created in a Firestore collection.
The following search on SO will return many code examples: https://stackoverflow.com/search?q=Firebase+Cloud+Functions+stripe
I have this JWT authorization configuration in my Startup.cs:
services.AddAuthentication(opts =>
{
opts.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opts.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
opts.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(opts =>
{
opts.RequireHttpsMetadata = false;
opts.SaveToken = true;
opts.TokenValidationParameters = new TokenValidationParameters()
{
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("my_secret_key")),
ValidIssuer = "iss",
ValidAudience = "aud",
ValidateIssuerSigningKey = true,
ValidateLifetime = true
};
});
My HomeController has [Authorize] attribute. So upon access to Home/Index, I get a 401 response and I am presented with a blank page. I want to redirect to my Account/LogIn page but I am not sure how to do it.
I read that this shouldn't automatically redirect because it won't make sense to API calls if they are not authorized and then you redirect them, so what is the proper way on how I would get them to the login page on 401.
Please bear in mind that in this project, I have both Web API and Action methods with [Authorize] attributes so I need to redirect only when it is an action method.
You may use StatusCodePages middleware. Add the following inot your Configure method:
app.UseStatusCodePages(async context => {
var request = context.HttpContext.Request;
var response = context.HttpContext.Response;
if (response.StatusCode == (int)HttpStatusCode.Unauthorized)
// you may also check requests path to do this only for specific methods
// && request.Path.Value.StartsWith("/specificPath")
{
response.Redirect("/account/login")
}
});
I read that this shouldn't automatically redirect because it won't make sense to API calls
this relates to API calls, that returns data other than pages. Let's say your app do call to API in the background. Redirect action to login page doesn't help, as app doesn't know how to authenticate itself in background without user involving.
Thanks for your suggestion... after spending a good time on google i could find your post and that worked for me. You raised a very good point because it does not make sense for app API calls.
However, I have a situation where the Actions called from the app has a specific notation route (/api/[Controller]/[Action]) which makes me possible to distinguish if my controller has been called by Browser or App.
app.UseStatusCodePages(async context =>
{
var request = context.HttpContext.Request;
var response = context.HttpContext.Response;
var path = request.Path.Value ?? "";
if (response.StatusCode == (int)HttpStatusCode.Unauthorized && path.StartsWith("/api", StringComparison.InvariantCultureIgnoreCase))
{
response.Redirect("~/Account/Login");
}
});
This works for both Razor Pages and MVC Views as follows: response.Redirect("/Login"); for Razor Pages. response.Redirect("/Home/Login"); for MVC Views. In order for this to work, the Authorize filter has to be added to the Controller. The code block also has to be added between app.UseAuthentication(); and app.UseAuthorization(); methods.
app.UseStatusCodePages(async context =>
{
var response = context.HttpContext.Response;
if (response.StatusCode == (int)HttpStatusCode.Unauthorized ||
response.StatusCode == (int)HttpStatusCode.Forbidden)
response.Redirect("/Home/Login");
});
Message displayed on chrome
Function to get the S3 URL
Future<String> getUrlS3(String UploadKey) async {
try {
GetUrlResult result =
await Amplify.Storage.getUrl(key: UploadKey);
return(result.url);
} on StorageException catch (e) {
return(e.message);
}
}
Function to get url
Future<void> cet() async {
String store = await S3Helper.getUrlS3('cover807c3f99-2bcb-44e3-be80-4899ea2355d8Attac');
print(store);
}
When I click on the link I get the access denied page.
The only way i can see the Objects are if i make the specific object public, also i am logged in via Auth Cognito so i should have the permissions to view them
I think you need to pass accesslevel option to your getUrlS3.
something like this
S3DownloadFileOptions options = S3UploadFileOptions(
accessLevel: StorageAccessLevel.protected,
metadata: metadata,
);
I'm new to flutter. Basically I'm using code Igniter framework for my web application. I created REST API for my web app, after user login using API all the methods check for the session_id if it exists then it proceeds, and if it doesn't then it gives
{ ['status'] = false, ['message'] = 'unauthorized access' }
I'm creating app with flutter, when i use the http method of flutter it changes session on each request. I mean, it doesn't maintain the session. I think it destroys and creates new connection each time. Here is thr class method which i use for api calls get and post request.
class ApiCall {
static Map data;
static List keys;
static Future<Map> getData(url) async {
http.Response response = await http.get(url);
Map body = JSON.decode(response.body);
data = body;
return body;
}
static Future postData(url, data) async {
Map result;
http.Response response = await http.post(url, body: data).then((response) {
result = JSON.decode(response.body);
}).catchError((error) => print(error.toString()));
data = result;
keys = result.keys.toList();
return result;
}
I want to make API request and then store session_id,
And is it possible to maintain session on the server so i can manage authentication on the web app it self.?
HTTP is a stateless protocol, so servers need some way to identify clients on the second, third and subsequent requests they make to the server. In your case you might authenticate using the first request, so you want the server to remember you on subsequent requests, so that it knows you are already authenticated. A common way to do this is with cookies.
Igniter sends a cookie with the session id. You need to gather this from each response and send it back in the next request. (Servers sometimes change the session id (to reduce things like clickjacking that we don't need to consider yet), so you need to keep extracting the cookie from every response.)
The cookie arrives as an HTTP response header called set-cookie (there may be more than one, though hopefully not for simplicity). To send the cookie back you add a HTTP request header to your subsequent requests called cookie, copying across some of the information you extracted from the set-cookie header.
Hopefully, Igniter only sends one set-cookie header, but for debugging purposes you may find it useful to print them all by using response.headers.forEach((a, b) => print('$a: $b'));. You should find Set-Cookie: somename=abcdef; optional stuff. We need to extract the string up to, but excluding the ;, i.e. somename=abcdef
On the next, and subsequent requests, add a request header to your next request of {'Cookie': 'somename=abcdef'}, by changing your post command to:
http.post(url, body: data, headers:{'Cookie': cookie})
Incidentally, I think you have a mismatch of awaits and thens in your code above. Generally, you don't want statics in classes, if they should be top level functions instead. Instead you could create a cookie aware class like:
class Session {
Map<String, String> headers = {};
Future<Map> get(String url) async {
http.Response response = await http.get(url, headers: headers);
updateCookie(response);
return json.decode(response.body);
}
Future<Map> post(String url, dynamic data) async {
http.Response response = await http.post(url, body: data, headers: headers);
updateCookie(response);
return json.decode(response.body);
}
void updateCookie(http.Response response) {
String rawCookie = response.headers['set-cookie'];
if (rawCookie != null) {
int index = rawCookie.indexOf(';');
headers['cookie'] =
(index == -1) ? rawCookie : rawCookie.substring(0, index);
}
}
}
I have an AngularJS app which is trying to auth with my Web Api. I receive the below error during the first call to my server if the user does not exist in my database, but does not happen on subsequent calls to the same method once the user exists in my db. (relevant code at the bottom)
No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:1378' is therefore not allowed access. The response had HTTP status code 500.
The flow of the logic is:
AngularJS auths with Facebook when the user clicks login
App does an $http.post to my server for auth/login passing their credentials
Server polls Facebook API for user details
If user exists, update their profile and auth 'em
Else, create new membership user, update with FB details, and auth 'em
The only thing that's different if they don't exist in the database (which is when the defect occurs) is that the login method asynchronously calls a createUser method then returns data. No additional external calls are made.
API startup method enabling CORS:
public void Configuration(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
var cors = new EnableCorsAttribute("*","*","*");
config.EnableCors(cors);
ConfigureOAuth(app);
app_start.WebApiConfig.Register(config);
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
app.UseWebApi(config);
}
API Controller:
[Route("Login")]
[HttpPost]
[AllowAnonymous]
public async Task<FacebookUserModel> Login(FacebookUserRequest user)
{
FacebookUserModel fbUser = new FacebookUserModel();
// Build FacebookUser object
try {
// Grab basic user details
string profileRequestUri = "https://graph.facebook.com/" + user.fbID + "?access_token=" + user.access_token;
HttpWebRequest profileRequest = (HttpWebRequest)WebRequest.Create(profileRequestUri);
profileRequest.Method = WebRequestMethods.Http.Get;
profileRequest.Accept = "application/json";
HttpWebResponse profileResponse = (HttpWebResponse)profileRequest.GetResponse();
Stream profileResponseStream = profileResponse.GetResponseStream();
StreamReader profileStreamReader = new StreamReader(profileResponseStream);
fbUser = JsonConvert.DeserializeObject<FacebookUserModel>(profileStreamReader.ReadToEnd());
} catch (Exception) ...
try {
// Grab profile picture
string pictureRequestUri = "https://graph.facebook.com/" + user.fbID + "/picture";
HttpWebRequest pictureRequest = (HttpWebRequest)WebRequest.Create(pictureRequestUri);
pictureRequest.Method = WebRequestMethods.Http.Get;
HttpWebResponse pictureResponse = (HttpWebResponse)pictureRequest.GetResponse();
fbUser.profilePictureUri = pictureResponse.ResponseUri.ToString();
} catch (Exception) ...
// If user exists, change password to new token and return)
if(userExists)
{
try {
IdentityUser identityUser = _repo.FindUser(ID, pass).Result;
FacebookUserModel dbUser = db.FacebookUserObjects.First(u => u.identityUserID == identityUser.Id);
db.Entry(dbUser).CurrentValues.SetValues(fbUser);
db.SaveChangesAsync();
fbUser.identityUserID = identityUser.Id;
return fbUser;
}
catch (Exception e)
{ return null; }
}
// Else, create the new user using same scheme
else
{
UserModel newUser = new UserModel
{
UserName = ID,
Password = pass,
ConfirmPassword = pass
};
// Create user in Identity & linked Facebook record
createUser(newUser, fbUser);
return fbUser;
}
}
private async void createUser(UserModel newUser, FacebookUserModel fbUser)
{
IdentityResult result = await _repo.RegisterUser(newUser);
var identityUser = await _repo.FindUser(newUser.UserName, newUser.Password);
fbUser.identityUserID = identityUser.Id;
db.FacebookUserObjects.Add(fbUser);
db.SaveChangesAsync();
}
AngularJS calls to my server:
var _login = function (fbID, fbToken) {
$http.post(serviceBase + 'auth/login', { "fbID": fbID, "access_token": fbToken }).then(function (response) {
var data = "grant_type=password&username=" + fbID + "&password=" + pass;
$http.post(serviceBase + 'auth/token', data, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } })
.success(function (tokenResponse) {
authServiceFactory.bearerToken = tokenResponse.access_token;
})
.error(function (err) {
console.log("token error:", err);
});
authServiceFactory.userObject = response.data;
window.localStorage['userObject'] = JSON.stringify(authServiceFactory.userObject);
})
};
Why would I get the No 'Access-Control-Allow-Origin' error only on the first call, but not subsequent ones?
Update
I have a workaround in place that works, but I don't really like. The issue only arose when calling a second method from my login controller, so if I moved that code up into the login controller instead of a secondary method it works without the CORS error. This really bothers me though and is inefficient, I'd love to know a better way around it.
if you're working with angularjs you might want to check out satellizer. It makes the auth process really simple and has some awesome built in window popup control.
As far as the Access-Control-Allow-Origin calls it could be happening because you explicitly set headers on the one call and the other ones are falling back to the default http provider? Check out $http and see if providing those defaults might work around it.