RXJS map operator returns empty response - asp.net

First, package.json :
"#angular/cli": "^6.0.8",
"rxjs": "6.2.1",
I am following a asp.net core 2 with angular tutorial. I had to adapt some code because it was thought for an older version.
The chapter is about creating JWT authentication.
I get the bearer token but when the api controller returns the response the auth.service gets an empty response and the line let token = res && res.token; goes false for the if so it never gets identified.
I suspect there is something wrong with the cast for TokenResponse but I cannot figure out what.
token.response.ts
interface TokenResponse {
token: string,
expiration: number
}
auth.service.ts
import { EventEmitter, Inject, Injectable, PLATFORM_ID } from "#angular/core";
import { isPlatformBrowser } from '#angular/common';
import { HttpClient, HttpHeaders } from "#angular/common/http";
import { Observable } from "rxjs";
import { map, catchError } from 'rxjs/operators';
import 'rxjs/Rx';
#Injectable()
export class AuthService {
authKey: string = "auth";
clientId: string = "NetCoreAngularWeb";
constructor(private http: HttpClient,
#Inject(PLATFORM_ID) private platformId: any) {
}
// performs the login
login(username: string, password: string): Observable<boolean> {
var url = "api/token/auth";
var data = {
username: username,
password: password,
client_id: this.clientId,
// required when signing up with username/password
grant_type: "password",
// space-separated list of scopes for which the token is
// issued
scope: "offline_access profile email"
};
return this.http.post<TokenResponse>(url, data)
.pipe(
map(res => {
let token = res && res.token;
// if the token is there, login has been successful
if (token) {
// store username and jwt token
this.setAuth(res);
// successful login
return true;
}
// failed login
return Observable.throw('Unauthorized');
}),
catchError(error => {
return new Observable<any>(error);
})
);
}
// performs the logout
logout(): boolean {
this.setAuth(null);
return true;
}
// Persist auth into localStorage or removes it if a NULL argument is given
setAuth(auth: TokenResponse | null): boolean {
if (isPlatformBrowser(this.platformId)) {
if (auth) {
localStorage.setItem(
this.authKey,
JSON.stringify(auth));
}
else {
localStorage.removeItem(this.authKey);
}
}
return true;
}
// Retrieves the auth JSON object (or NULL if none)
getAuth(): TokenResponse | null {
if (isPlatformBrowser(this.platformId)) {
var i = localStorage.getItem(this.authKey);
if (i) {
return JSON.parse(i);
}
}
return null;
}
// Returns TRUE if the user is logged in, FALSE otherwise.
isLoggedIn(): boolean {
if (isPlatformBrowser(this.platformId)) {
return localStorage.getItem(this.authKey) != null;
}
return false;
}
}
TokenController
[HttpPost("Auth")]
public async Task<IActionResult> Auth([FromBody]TokenRequestViewModel model)
{
// return a generic HTTP Status 500 (server Error)
// if the client payload is invalid.
if (model == null) return new StatusCodeResult(500);
switch (model.grant_type)
{
case "password":
return await GetToken(model);
default:
// not supported - return a HTTP 401 (Unauthorized)
return new UnauthorizedResult();
}
}
private async Task<IActionResult> GetToken(TokenRequestViewModel model)
{
try
{
// check if there's an user with the given username
var user = await UserManager.FindByNameAsync(model.username);
// fallback to support e-mail address instead of username
if (user == null && model.username.Contains("#"))
user = await UserManager.FindByEmailAsync(model.username);
if (user == null || !await UserManager.CheckPasswordAsync(user, model.password))
{
// user does not exists or password mismatch
return new UnauthorizedResult();
}
// username & password matches: create and return the Jwt token
DateTime now = DateTime.UtcNow;
// add the registered claims for JWT (RFC7519)
// For more info, see...
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.Id),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Iat, new DateTimeOffset(now).ToUnixTimeSeconds().ToString())
// TODO add additional claims here
};
var tokenExpirationMins = Configuration.GetValue<int>("Auth:Jwt:TokenExpirationInMinutes");
var issuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Auth:Jwt:Key"]));
var token = new JwtSecurityToken(
issuer: Configuration["Auth:Jwt:Issuer"],
audience: Configuration["Auth:Jwt:Audicnete"],
claims: claims,
notBefore: now,
expires:
now.Add(TimeSpan.FromMinutes(tokenExpirationMins)),
signingCredentials: new SigningCredentials(issuerSigningKey, SecurityAlgorithms.HmacSha256)
);
var encodedToken = new JwtSecurityTokenHandler().WriteToken(token);
// build & return the response
var response = new TokenResponseViewModel()
{
token = encodedToken,
expiration = tokenExpirationMins
};
return Json(response);
}
catch (Exception ex)
{
return new UnauthorizedResult();
}
}
EDIT - response, inspected with POSTMAN
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJlODBiNzRkMi0xOGIwLTQ5NzUtOWUyYS0wOGJiOGJlOTUzMDEiLCJqdGkiOiIzNTkzNjVhYS02M2UyLTQ3ZGMtYjY1NC1mMDk3YWQxNjNmMzQiLCJpYXQiOiIxNTMwNzEzMzU2IiwibmJmIjoxNTMwNzEzMzU2LCJleHAiOjE1MzU4OTczNTYsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzE3NzkvIn0.6QlGCK_4ybzYEz8qaRDDotOja6x0FI0fZTxmky94S5Y",
"expiration": 86400
}
Status 200 OK
Time:11281 ms
Size:671 B
Content-Type →application/json; charset=utf-8
Date →Wed, 04 Jul 2018 14:09:21 GMT
Server →Kestrel
Transfer-Encoding →chunked
X-Powered-By →ASP.NET

Well, there were a couple of issues here.
First, my version of Chrome was breaking the VS Studio Debugger. I had to update it to a 63+ Version (65 in my case)
https://social.msdn.microsoft.com/Forums/vstudio/en-US/5cd1aa58-d986-4d03-a10e-bf1fd62080a7/simultaneous-debugging-in-chrome?forum=vsdebug#5cd1aa58-d986-4d03-a10e-bf1fd62080a7
After this, I could debug better and I found the errors.
1- auth.service.ts . throwError function in Observable
https://www.academind.com/learn/javascript/rxjs-6-what-changed/
return this.http.post<TokenResponse>(url, data)
.pipe(
map(res => {
let token = res && res.token;
if (token) {
this.setAuth(res);
return true;
}
else {
//failed login
return Observable.throwError('Unauthorized');
}
}),
catchError(error => {
//console.log(error);
return new Observable<any>(error);
})
);
2- Typo error in TokenController Audiecnete instead of Audience
var token = new JwtSecurityToken(
issuer: Configuration["Auth:Jwt:Issuer"],
audience: Configuration["Auth:Jwt:Audience"],
claims: claims,
notBefore: now,
expires:
now.Add(TimeSpan.FromMinutes(tokenExpirationMins)),
signingCredentials: new SigningCredentials(issuerSigningKey, SecurityAlgorithms.HmacSha256)
);

Related

How to get logged user in vuejs

I made a jwt authetication using asp.net core and vuejs
this is my auth controller :
[Route("Login")]
[HttpPost]
public IActionResult Login(LoginArgument loginArgument)
{
var user = _userService.GetByEmail(loginArgument.Email);
if (user == null) return BadRequest(error: new { message = "Invalid credential : verify email" });
if (!BC.BCrypt.Verify(text: loginArgument.Password, hash: user.Password))
{
return BadRequest(error: new { message = "Invalid credential : verify password" });
}
var jwt= _jwtService.Generate(user.Id);
Response.Cookies.Append(key: "jwt", value: jwt, new Microsoft.AspNetCore.Http.CookieOptions
{
HttpOnly=false,
SameSite=Microsoft.AspNetCore.Http.SameSiteMode.None
}) ;
return Ok(user);
}
[Route("User")]
[HttpGet]
public IActionResult User()
{
try
{
var jwt = Request.Cookies["jwt"];
var token = _jwtService.Verify(jwt);
int userId = int.Parse(token.Issuer);
var user = _userService.GetById(userId);
return Ok(user);
}
catch (Exception)
{
return Unauthorized();
}
}
and this is the login in vue
<script lang="ts">
import { reactive } from 'vue';
import { useRouter } from "vue-router";
export default {
name: "Login",
setup() {
const data = reactive({
email: '',
password: ''
});
const router = useRouter();
const submit = async () => {
await fetch('https://localhost:44391/api/Auth/Login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(data)
});
await router.push('/Countries');
}
return {
data,
submit
}
},
}
the login part is working in front and back sides perfectly and i can see the cookies
the problem is when i try to get the logged user. in back side i can get it successfully but in front it says that no user is logged
here is the loggedUser vue
<script lang="ts">
import { onMounted, ref } from 'vue';
export default {
name: "LoggedUser",
setup() {
const message = ref('You are not logged in!');
onMounted(async () => {
const response = await fetch('https://localhost:44391/api/Auth/User', {
headers: { 'Content-Type': 'application/json' },
credentials: 'include'
});
const content = await response.json();
message.value = `hi ${content.name}`;
});
return {
message
}
}
}
Here is the errors i got when i inspect the page :
this issues appear the moment of login
1- Mark cross-site cookies as Secure to allow setting them in cross-site contexts
2- Migrate entirely to HTTPS to have cookies sent to same-site subresources
this one appears when i call loggedUser in front even so it works in the back side
{type: "https://tools.ietf.org/html/rfc7235#section-3.1", title: "Unauthorized", status:
401,…}
status: 401
title: "Unauthorized"
traceId: "00-b4a9f6fee8dff6439952ded0bb50005d-43c9aee84c454b40-00"
type: "https://tools.ietf.org/html/rfc7235#section-3.1"
You need to send the access token in the request headers
Example:
let token = '???'
const response = await post('localhost/api/auth/user', {
headers: {
'Content-Type': 'application/json'
'Authorization' : 'Bearer '+ token
}
});

No authentication handler is registered for the scheme 'Cookies'. The registered schemes are: Application, Bearer, ASOS

I am implementing Aspnet.security.openidconnect (ASOS) with .net core 2.1 application. Now the issue is when I am trying to execute this chunk in controller,
public async Task<IActionResult> Authorize()
{
if (Response.StatusCode != 200)
{
return View("AuthorizeError");
}
var ticket = await AuthenticationHttpContextExtensions.AuthenticateAsync(HttpContext, CookieAuthenticationDefaults.AuthenticationScheme);
var identity = ticket != null && ticket.Principal != null ? ticket.Ticket.Principal : null;
if (identity == null)
{
await AuthenticationHttpContextExtensions.ChallengeAsync(HttpContext, CookieAuthenticationDefaults.AuthenticationScheme, null);
return Unauthorized();
}
ViewData["Name"] = ticket.Principal.Identity.Name;
var scopes = (HttpContext.Request.Query["scope"].ToString() ?? "").Split(' ');
ViewData["Scopes"] = scopes;
//var claimsIdentity = new ClaimsIdentity(identity.Claims, "Bearer", identity.NameClaimType, identity.RoleClaimType);
var claimsIdentity = new ClaimsIdentity(identity.Claims, "Bearer");
foreach (var scope in scopes)
{
claimsIdentity.AddClaim(new Claim("urn:oauth:scope", scope));
}
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
await AuthenticationHttpContextExtensions.SignInAsync(HttpContext, claimsPrincipal);
logger.Info("Authorize request received");
return View();
}
The error I am getting on this line:
var ticket = await AuthenticationHttpContextExtensions.AuthenticateAsync(HttpContext, CookieAuthenticationDefaults.AuthenticationScheme);
And here is the implementation of ASOS in startup:
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie("Application", options =>
{
options.LoginPath = new PathString(LoginPath);
options.LogoutPath = new PathString(LogoutPath);
options.ExpireTimeSpan = TimeSpan.FromMinutes(5);
//options.AccessDeniedPath = new PathString();
});
//services.AddAuthentication("External")
// .AddCookie("Cookies", options =>
// {
// options.Cookie.Name = CookieAuthenticationDefaults.CookiePrefix + "External";
// options.ExpireTimeSpan = TimeSpan.FromMinutes(5);
// });
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();
services.AddAuthentication(OAuthValidationDefaults.AuthenticationScheme).AddOAuthValidation()
.AddOpenIdConnectServer(options =>
{
options.AuthorizationEndpointPath = new PathString(AuthorizePath);
// Enable the token endpoint.
options.TokenEndpointPath = new PathString(TokenPath);
options.ApplicationCanDisplayErrors = true;
options.AccessTokenLifetime = TimeSpan.FromMinutes(5);
#if DEBUG
options.AllowInsecureHttp = true;
#endif
options.Provider.OnValidateAuthorizationRequest = context =>
{
if (string.Equals(context.ClientId, Configuration["OpenIdServer:ClientId"], StringComparison.Ordinal))
{
context.Validate(context.RedirectUri);
}
return Task.CompletedTask;
};
// Implement OnValidateTokenRequest to support flows using the token endpoint.
options.Provider.OnValidateTokenRequest = context =>
{
// Reject token requests that don't use grant_type=password or grant_type=refresh_token.
if (!context.Request.IsClientCredentialsGrantType() && !context.Request.IsPasswordGrantType()
&& !context.Request.IsRefreshTokenGrantType())
{
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedGrantType,
description: "Only grant_type=password and refresh_token " +
"requests are accepted by this server.");
return Task.CompletedTask;
}
if (string.IsNullOrEmpty(context.ClientId))
{
context.Skip();
return Task.CompletedTask;
}
if (string.Equals(context.ClientId, Configuration["OpenIdServer:ClientId"], StringComparison.Ordinal) &&
string.Equals(context.ClientSecret, Configuration["OpenIdServer:ClientSecret"], StringComparison.Ordinal))
{
context.Validate();
}
return Task.CompletedTask;
};
// Implement OnHandleTokenRequest to support token requests.
options.Provider.OnHandleTokenRequest = context =>
{
// Only handle grant_type=password token requests and let
// the OpenID Connect server handle the other grant types.
if (context.Request.IsClientCredentialsGrantType() || context.Request.IsPasswordGrantType())
{
//var identity = new ClaimsIdentity(context.Scheme.Name,
// OpenIdConnectConstants.Claims.Name,
// OpenIdConnectConstants.Claims.Role);
ClaimsIdentity identity = null;
if (context.Request.IsClientCredentialsGrantType())
{
identity = new ClaimsIdentity(new GenericIdentity(context.Request.ClientId, "Bearer"), context.Request.GetScopes().Select(x => new Claim("urn:oauth:scope", x)));
}
else if (context.Request.IsPasswordGrantType())
{
identity = new ClaimsIdentity(new GenericIdentity(context.Request.Username, "Bearer"), context.Request.GetScopes().Select(x => new Claim("urn:oauth:scope", x)));
}
// Add the mandatory subject/user identifier claim.
// By default, claims are not serialized in the access/identity tokens.
// Use the overload taking a "destinations" parameter to make sure
// your claims are correctly inserted in the appropriate tokens.
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, Guid.NewGuid().ToString("n") + Guid.NewGuid().ToString("n"), OpenIdConnectConstants.Destinations.AccessToken, OpenIdConnectConstants.Destinations.IdentityToken);
var ticket = new Microsoft.AspNetCore.Authentication.AuthenticationTicket(
new ClaimsPrincipal(identity),
new Microsoft.AspNetCore.Authentication.AuthenticationProperties(),
context.Scheme.Name);
// Call SetScopes with the list of scopes you want to grant
// (specify offline_access to issue a refresh token).
ticket.SetScopes(
OpenIdConnectConstants.Scopes.Profile,
OpenIdConnectConstants.Scopes.OfflineAccess);
context.Validate(ticket);
}
return Task.CompletedTask;
};
Now the error I am getting is:
InvalidOperationException: No authentication handler is registered for
the scheme 'Cookies'. The registered schemes are: Application, Bearer,
ASOS. Did you forget to call
AddAuthentication().AddSomeAuthHandler?
What am I missing here. Any help?
So found the issue, actually I was using "Application" name for cookie scheme and in controller I was using default name "Cookies". So just had to remove the explicit "Application" name to default "Cookies" name
No authenticationScheme was specified, and there was no DefaultChallengeScheme found Cookies Authentication
In my case I was using "Cookies" when adding authentication and "Cookie" when calling the SiginOut method.
Changed both the places to use "Cookies"
Startup:
services.AddAuthentication(config => {
config.DefaultScheme = "Cookies";
config.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")<---- Change here.
.AddOpenIdConnect("oidc", config => {
config.Authority = "https://localhost:44392/";
config.ClientId = "client_id_mvc";
config.ClientSecret = "client_secret_mvc";
config.SaveTokens = true;
config.ResponseType = "code";
//config.SignedOutCallbackPath = "/Privacy";
});
Calling SignOut:
public async Task<IActionResult> OnPostAsync()
{
return SignOut("Cookies", "oidc");
}

Using Interceptor in Dio for Flutter to Refresh Token

I am trying to use Interceptor with Dio in flutter, I have to handle Token expire.
following is my code
Future<Dio> getApiClient() async {
token = await storage.read(key: USER_TOKEN);
_dio.interceptors.clear();
_dio.interceptors
.add(InterceptorsWrapper(onRequest: (RequestOptions options) {
// Do something before request is sent
options.headers["Authorization"] = "Bearer " + token;
return options;
},onResponse:(Response response) {
// Do something with response data
return response; // continue
}, onError: (DioError error) async {
// Do something with response error
if (error.response?.statusCode == 403) {
// update token and repeat
// Lock to block the incoming request until the token updated
_dio.interceptors.requestLock.lock();
_dio.interceptors.responseLock.lock();
RequestOptions options = error.response.request;
FirebaseUser user = await FirebaseAuth.instance.currentUser();
token = await user.getIdToken(refresh: true);
await writeAuthKey(token);
options.headers["Authorization"] = "Bearer " + token;
_dio.interceptors.requestLock.unlock();
_dio.interceptors.responseLock.unlock();
_dio.request(options.path, options: options);
} else {
return error;
}
}));
_dio.options.baseUrl = baseUrl;
return _dio;
}
problem is instead of repeating the network call with the new token, Dio is returning the error object to the calling method, which in turn is rendering the wrong widget, any leads on how to handle token refresh with dio?
I have found a simple solution that looks like the following:
this.api = Dio();
this.api.interceptors.add(InterceptorsWrapper(
onError: (error) async {
if (error.response?.statusCode == 403 ||
error.response?.statusCode == 401) {
await refreshToken();
return _retry(error.request);
}
return error.response;
}));
Basically what is going on is it checks to see if the error is a 401 or 403, which are common auth errors, and if so, it will refresh the token and retry the response. My implementation of refreshToken() looks like the following, but this may vary based on your api:
Future<void> refreshToken() async {
final refreshToken = await this._storage.read(key: 'refreshToken');
final response =
await this.api.post('/users/refresh', data: {'token': refreshToken});
if (response.statusCode == 200) {
this.accessToken = response.data['accessToken'];
}
}
I use Flutter Sercure Storage to store the accessToken. My retry method looks like the following:
Future<Response<dynamic>> _retry(RequestOptions requestOptions) async {
final options = new Options(
method: requestOptions.method,
headers: requestOptions.headers,
);
return this.api.request<dynamic>(requestOptions.path,
data: requestOptions.data,
queryParameters: requestOptions.queryParameters,
options: options);
}
If you want to easily allows add the access_token to the request I suggest adding the following function when you declare your dio router with the onError callback:
onRequest: (options) async {
options.headers['Authorization'] = 'Bearer: $accessToken';
return options;
},
I solved it using interceptors in following way :-
Future<Dio> getApiClient() async {
token = await storage.read(key: USER_TOKEN);
_dio.interceptors.clear();
_dio.interceptors
.add(InterceptorsWrapper(onRequest: (RequestOptions options) {
// Do something before request is sent
options.headers["Authorization"] = "Bearer " + token;
return options;
},onResponse:(Response response) {
// Do something with response data
return response; // continue
}, onError: (DioError error) async {
// Do something with response error
if (error.response?.statusCode == 403) {
_dio.interceptors.requestLock.lock();
_dio.interceptors.responseLock.lock();
RequestOptions options = error.response.request;
FirebaseUser user = await FirebaseAuth.instance.currentUser();
token = await user.getIdToken(refresh: true);
await writeAuthKey(token);
options.headers["Authorization"] = "Bearer " + token;
_dio.interceptors.requestLock.unlock();
_dio.interceptors.responseLock.unlock();
return _dio.request(options.path,options: options);
} else {
return error;
}
}));
_dio.options.baseUrl = baseUrl;
return _dio;
}
Dio 4.0.0 Support
dio.interceptors.add(
InterceptorsWrapper(
onRequest: (request, handler) {
if (token != null && token != '')
request.headers['Authorization'] = 'Bearer $token';
return handler.next(request);
},
onError: (e, handler) async {
if (e.response?.statusCode == 401) {
try {
await dio
.post(
"https://refresh.api",
data: jsonEncode(
{"refresh_token": refreshtoken}))
.then((value) async {
if (value?.statusCode == 201) {
//get new tokens ...
print("access token" + token);
print("refresh token" + refreshtoken);
//set bearer
e.requestOptions.headers["Authorization"] =
"Bearer " + token;
//create request with new access token
final opts = new Options(
method: e.requestOptions.method,
headers: e.requestOptions.headers);
final cloneReq = await dio.request(e.requestOptions.path,
options: opts,
data: e.requestOptions.data,
queryParameters: e.requestOptions.queryParameters);
return handler.resolve(cloneReq);
}
return e;
});
return dio;
} catch (e, st) {
}
}
},
),
);
I modify John Anderton's answer. I agree that it is better approach to check the token(s) before you actually make the request. we have to check if the tokens are expired or not, instead of making request and check the error 401 and 403.
I modify it to add some functionalities, so this interceptor can be used
to add access token to the header if it is still valid
to regenerate access token if it has expired
to navigate back to Login Page if refresh token has expired
to navigate back to Login Page if there is an error because of invalidated token (for example, revoked by the backend)
and it also work for multiple concurrent requests, and if you don't need to add token to the header (like in login endpoint), this interceptor can handle it as well. here is the interceptor
class AuthInterceptor extends Interceptor {
final Dio _dio;
final _localStorage = LocalStorage.instance; // helper class to access your local storage
AuthInterceptor(this._dio);
#override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
if (options.headers["requiresToken"] == false) {
// if the request doesn't need token, then just continue to the next interceptor
options.headers.remove("requiresToken"); //remove the auxiliary header
return handler.next(options);
}
// get tokens from local storage, you can use Hive or flutter_secure_storage
final accessToken = _localStorage.getAccessToken();
final refreshToken = _localStorage.getRefreshToken();
if (accessToken == null || refreshToken == null) {
_performLogout(_dio);
// create custom dio error
options.extra["tokenErrorType"] = TokenErrorType.tokenNotFound; // I use enum type, you can chage it to string
final error = DioError(requestOptions: options, type: DioErrorType.other);
return handler.reject(error);
}
// check if tokens have already expired or not
// I use jwt_decoder package
// Note: ensure your tokens has "exp" claim
final accessTokenHasExpired = JwtDecoder.isExpired(accessToken);
final refreshTokenHasExpired = JwtDecoder.isExpired(refreshToken);
var _refreshed = true;
if (refreshTokenHasExpired) {
_performLogout(_dio);
// create custom dio error
options.extra["tokenErrorType"] = TokenErrorType.refreshTokenHasExpired;
final error = DioError(requestOptions: options, type: DioErrorType.other);
return handler.reject(error);
} else if (accessTokenHasExpired) {
// regenerate access token
_dio.interceptors.requestLock.lock();
_refreshed = await _regenerateAccessToken();
_dio.interceptors.requestLock.unlock();
}
if (_refreshed) {
// add access token to the request header
options.headers["Authorization"] = "Bearer $accessToken";
return handler.next(options);
} else {
// create custom dio error
options.extra["tokenErrorType"] = TokenErrorType.failedToRegenerateAccessToken;
final error = DioError(requestOptions: options, type: DioErrorType.other);
return handler.reject(error);
}
}
#override
void onError(DioError err, ErrorInterceptorHandler handler) {
if (err.response?.statusCode == 403 || err.response?.statusCode == 401) {
// for some reasons the token can be invalidated before it is expired by the backend.
// then we should navigate the user back to login page
_performLogout(_dio);
// create custom dio error
err.type = DioErrorType.other;
err.requestOptions.extra["tokenErrorType"] = TokenErrorType.invalidAccessToken;
}
return handler.next(err);
}
void _performLogout(Dio dio) {
_dio.interceptors.requestLock.clear();
_dio.interceptors.requestLock.lock();
_localStorage.removeTokens(); // remove token from local storage
// back to login page without using context
// check this https://stackoverflow.com/a/53397266/9101876
navigatorKey.currentState?.pushReplacementNamed(LoginPage.routeName);
_dio.interceptors.requestLock.unlock();
}
/// return true if it is successfully regenerate the access token
Future<bool> _regenerateAccessToken() async {
try {
var dio = Dio(); // should create new dio instance because the request interceptor is being locked
// get refresh token from local storage
final refreshToken = _localStorage.getRefreshToken();
// make request to server to get the new access token from server using refresh token
final response = await dio.post(
"https://yourDomain.com/api/refresh",
options: Options(headers: {"Authorization": "Bearer $refreshToken"}),
);
if (response.statusCode == 200 || response.statusCode == 201) {
final newAccessToken = response.data["accessToken"]; // parse data based on your JSON structure
_localStorage.saveAccessToken(newAccessToken); // save to local storage
return true;
} else if (response.statusCode == 401 || response.statusCode == 403) {
// it means your refresh token no longer valid now, it may be revoked by the backend
_performLogout(_dio);
return false;
} else {
print(response.statusCode);
return false;
}
} on DioError {
return false;
} catch (e) {
return false;
}
}
}
usage
final dio = Dio();
dio.options.baseUrl = "https://yourDomain.com/api";
dio.interceptors.addAll([
AuthInterceptor(dio), // add this line before LogInterceptor
LogInterceptor(),
]);
if your request doesn't need token in the header (like in the login endpoint), then you should make request like this
await dio.post(
"/login",
data: loginData,
options: Options(headers: {"requiresToken": false}), // add this line
);
otherwise, just make a regular request without adding token to the header option, the interceptor will automatically handle it.
await dio.get("/user", data: myData);
I think that a better approach is to check the token(s) before you actually make the request. That way you have less network traffic and the response is faster.
EDIT: Another important reason to follow this approach is because it is a safer one, as X.Y. pointed out in the comment section
In my example I use:
http: ^0.13.3
dio: ^4.0.0
flutter_secure_storage: ^4.2.0
jwt_decode: ^0.3.1
flutter_easyloading: ^3.0.0
The idea is to first check the expiration of tokens (both access and refresh).
If the refresh token is expired then clear the storage and redirect to LoginPage.
If the access token is expired then (before submit the actual request) refresh it by using the refresh token, and then use the refreshed credentials to submit the original request. In that way you minimize the network traffic and you take the response way faster.
I did this:
AuthService appAuth = new AuthService();
class AuthService {
Future<void> logout() async {
token = '';
refresh = '';
await Future.delayed(Duration(milliseconds: 100));
Navigator.of(cnt).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => LoginPage()),
(_) => false,
);
}
Future<bool> login(String username, String password) async {
var headers = {'Accept': 'application/json'};
var request = http.MultipartRequest('POST', Uri.parse(baseURL + 'token/'));
request.fields.addAll({'username': username, 'password': password});
request.headers.addAll(headers);
http.StreamedResponse response = await request.send();
if (response.statusCode == 200) {
var resp = await response.stream.bytesToString();
final data = jsonDecode(resp);
token = data['access'];
refresh = data['refresh'];
secStore.secureWrite('token', token);
secStore.secureWrite('refresh', refresh);
return true;
} else {
return (false);
}
}
Future<bool> refreshToken() async {
var headers = {'Accept': 'application/json'};
var request =
http.MultipartRequest('POST', Uri.parse(baseURL + 'token/refresh/'));
request.fields.addAll({'refresh': refresh});
request.headers.addAll(headers);
http.StreamedResponse response = await request.send();
if (response.statusCode == 200) {
final data = jsonDecode(await response.stream.bytesToString());
token = data['access'];
refresh = data['refresh'];
secStore.secureWrite('token', token);
secStore.secureWrite('refresh', refresh);
return true;
} else {
print(response.reasonPhrase);
return false;
}
}
}
After that create the interceptor
import 'package:dio/dio.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import '../settings/globals.dart';
class AuthInterceptor extends Interceptor {
static bool isRetryCall = false;
#override
void onRequest(
RequestOptions options, RequestInterceptorHandler handler) async {
bool _token = isTokenExpired(token);
bool _refresh = isTokenExpired(refresh);
bool _refreshed = true;
if (_refresh) {
appAuth.logout();
EasyLoading.showInfo(
'Expired session');
DioError _err;
handler.reject(_err);
} else if (_token) {
_refreshed = await appAuth.refreshToken();
}
if (_refreshed) {
options.headers["Authorization"] = "Bearer " + token;
options.headers["Accept"] = "application/json";
handler.next(options);
}
}
#override
void onResponse(Response response, ResponseInterceptorHandler handler) async {
handler.next(response);
}
#override
void onError(DioError err, ErrorInterceptorHandler handler) async {
handler.next(err);
}
}
The secure storage functionality is from:
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
SecureStorage secStore = new SecureStorage();
class SecureStorage {
final _storage = FlutterSecureStorage();
void addNewItem(String key, String value) async {
await _storage.write(
key: key,
value: value,
iOptions: _getIOSOptions(),
);
}
IOSOptions _getIOSOptions() => IOSOptions(
accountName: _getAccountName(),
);
String _getAccountName() => 'blah_blah_blah';
Future<String> secureRead(String key) async {
String value = await _storage.read(key: key);
return value;
}
Future<void> secureDelete(String key) async {
await _storage.delete(key: key);
}
Future<void> secureWrite(String key, String value) async {
await _storage.write(key: key, value: value);
}
}
check expiration with:
bool isTokenExpired(String _token) {
DateTime expiryDate = Jwt.getExpiryDate(_token);
bool isExpired = expiryDate.compareTo(DateTime.now()) < 0;
return isExpired;
}
and then the original request
var dio = Dio();
Future<Null> getTasks() async {
EasyLoading.show(status: 'Wait ...');
Response response = await dio
.get(baseURL + 'tasks/?task={"foo":"1","bar":"30"}');
if (response.statusCode == 200) {
print('success');
} else {
print(response?.statusCode);
}}
As you can see the Login and refreshToken request use http package (they don't need the interceptor). The getTasks use dio and it's interceptor in order to get its response in one and only request
Dio 4.0.0
dio.interceptors.clear();
dio.interceptors.add(
InterceptorsWrapper(
onRequest: (request, handler) {
if (token != null && token != '')
request.headers['Authorization'] = 'Bearer $token';
return handler.next(request);
},
onError: (err, handler) async {
if (err.response?.statusCode == 401) {
try {
await dio
.post(
"https://refresh.api",
data: jsonEncode(
{"refresh_token": refreshtoken}))
.then((value) async {
if (value?.statusCode == 201) {
//get new tokens ...
print("acces token" + token);
print("refresh token" + refreshtoken);
//set bearer
err.requestOptions.headers["Authorization"] =
"Bearer " + token;
//create request with new access token
final opts = new Options(
method: err.requestOptions.method,
headers: err.requestOptions.headers);
final cloneReq = await dio.request(err.requestOptions.path,
options: opts,
data: err.requestOptions.data,
queryParameters: err.requestOptions.queryParameters);
return handler.resolve(cloneReq);
}
return err;
});
return dio;
} catch (err, st) {
}
}
},
),
);
Dio 4.0.2 deprecates Interceptor locks. QueuedInterceptor should be used instead.
From the docs:
Locks of interceptors were originally designed to synchronize interceptor execution, but locks have a problem that once it becomes unlocked all of the requests run at once, rather than executing sequentially. Now QueuedInterceptor can do it better.
QueuedInterceptor provides a mechanism for sequential access(one by one) to interceptors.
An example of AuthInterceptor implemented using QueuedInterceptor:
/// Adds Authorization header with a non-expired bearer token.
///
/// Logic:
/// 1. Check if the endpoint requires authentication
/// - If not, bypass interceptor
/// 2. Get a non-expired access token
/// - AuthRepository takes care of refreshing the token if it is expired
/// 3. Make API call (attaching token in Authorization header)
/// 4. If response if 401 (e.g. a not expired access token that was revoked by backend),
/// force refresh access token and retry call.
///
/// For non-authenticated endpoints add the following header to bypass this interceptor:
/// `Authorization: None`
///
/// For endpoints with optional authentication provide the following header:
/// `Authorization: Optional`
/// - If user is not authenticated: the Authorization header will be removed
/// and the call will be performed without it.
/// - If the user is authenticated: the authentication token will be attached in the
/// Authorization header.
class AuthInterceptor extends QueuedInterceptor {
AuthInterceptor({
required this.dio,
required this.authRepository,
this.retries = 3,
});
/// The original dio
final Dio dio;
final AuthRepository authRepository;
/// The number of retries in case of 401
final int retries;
#override
Future<void> onRequest(
final RequestOptions options,
final RequestInterceptorHandler handler,
) async {
// Non-authenticated endpoint -> bypass this interceptor
if (options._requiresNoAuthentication()) {
options._removeAuthenticationHeader();
return handler.next(options);
}
// Get auth token
final authTokenRes = await authRepository.getAuthToken();
authTokenRes.fold(
success: (final authToken) {
// Add auth token in Authorization header
options._setAuthenticationHeader(authToken.token);
handler.next(options);
},
failure: (final e) async {
// Skip authentication header if it is optional and user is not authenticated
if (e is UserNoAuthenticatedException && options._hasOptionalAuthentication()) {
options._removeAuthenticationHeader();
return handler.next(options);
}
// Handle auth token errors
await _onErrorRefreshingToken();
final error = DioError(requestOptions: options, error: e);
handler.reject(error);
},
);
}
#override
Future<void> onError(final DioError err, final ErrorInterceptorHandler handler) async {
if (err.response?.statusCode != 401) {
return super.onError(err, handler);
}
// Check retry attempt
final attempt = err.requestOptions._retryAttempt + 1;
if (attempt > retries) {
return super.onError(err, handler);
}
err.requestOptions._retryAttempt = attempt;
await Future<void>.delayed(const Duration(seconds: 1));
// Force refresh auth token
final authTokenRes = await authRepository.getAuthToken(forceRefresh: true);
authTokenRes.fold(
success: (final authToken) async {
// Add new auth token in Authorization header and retry call
try {
final options = err.requestOptions.._setAuthenticationHeader(authToken.token);
final response = await dio.fetch<void>(options);
handler.resolve(response);
} on DioError catch (e) {
if (e.response?.statusCode == 401) {
await _onErrorRefreshingToken();
}
super.onError(e, handler);
}
},
failure: (final e) async {
// Handle auth token errors
await _onErrorRefreshingToken();
final error = DioError(requestOptions: err.requestOptions, error: authTokenRes.error);
return handler.next(error);
},
);
}
Future<void> _onErrorRefreshingToken() async {
await authRepository.signOut();
}
}
extension AuthRequestOptionsX on RequestOptions {
bool _requiresNoAuthentication() => headers['Authorization'] == 'None';
bool _hasOptionalAuthentication() => headers['Authorization'] == 'Optional';
void _setAuthenticationHeader(final String token) => headers['Authorization'] = 'Bearer $token';
void _removeAuthenticationHeader() => headers.remove('Authorization');
int get _retryAttempt => (extra['auth_retry_attempt'] as int?) ?? 0;
set _retryAttempt(final int attempt) => extra['auth_retry_attempt'] = attempt;
}
Notes:
In my case AuthRepository is a wrapper of FirebaseAuth. The Firebase SDK takes care of providing a non-expired token when getAuthToken() is called.
AuthRepository.getAuthToken() returns a Future<Result<AuthToken, AuthException>>. My Result object is similar to the one provided in Result package.
You would get a response status code as 401 for token expiration. In order to request new access token, you need to use post method along with form data and required Dio's options (content-type and headers). Below is the code shows how to request new token.
After successful request, if you get the response status code as 200, then you will get new access token value along with refresh token value and save them in any storage you prefer to use. For example, Shared preferences.
Once you have new access token saved, you can use it to fetch data using get method shown in the same code below.
onError(DioError error) async {
if (error.response?.statusCode == 401) {
Response response;
var authToken = base64
.encode(utf8.encode("username_value" + ":" + "password_value"));
FormData formData = new FormData.from(
{"grant_type": "refresh_token", "refresh_token": refresh_token_value});
response = await dio.post(
url,
data: formData,
options: new Options(
contentType: ContentType.parse("application/x-www-form-urlencoded"),
headers: {HttpHeaders.authorizationHeader: 'Basic $authToken'}),
);
if (response.statusCode == 200) {
response = await dio.get(
url,
options: new Options(headers: {
HttpHeaders.authorizationHeader: 'Bearer access_token_value'
}),
);
return response;
} else {
print(response.data);
return null;
}
}
return error;
}
Below is a snippet from my interceptor
dio.interceptors
.add(InterceptorsWrapper(onRequest: (RequestOptions options) async {
/* Write your request logic setting your Authorization header from prefs*/
String token = await prefs.accessToken;
if (token != null) {
options.headers["Authorization"] = "Bearer " + token;
return options; //continue
}, onResponse: (Response response) async {
// Write your response logic
return response; // continue
}, onError: (DioError dioError) async {
// Refresh Token
if (dioError.response?.statusCode == 401) {
Response response;
var data = <String, dynamic>{
"grant_type": "refresh_token",
"refresh_token": await prefs.refreshToken,
'email': await prefs.userEmail
};
response = await dio
.post("api/url/for/refresh/token", data: data);
if (response.statusCode == 200) {
var newRefreshToken = response.data["data"]["refresh_token"]; // get new refresh token from response
var newAccessToken = response.data["data"]["access_token"]; // get new access token from response
prefs.refreshToken = newRefreshToken;
prefs.accessToken = newAccessToken; // to be used in the request section of the interceptor
return dio.request(dioError.request.baseUrl + dioError.request.path,
options: dioError.request);
}
}
return dioError;
}));
return dio;
}
}
it is working 100%
RestClient client;
static BaseOptions options = new BaseOptions(
connectTimeout: 5000,
receiveTimeout: 3000,
);
RemoteService() {
// or new Dio with a BaseOptions instance.
final dio = Dio(options);
dio.interceptors
.add(InterceptorsWrapper(onRequest: (RequestOptions options) async {
// Do something before request is sent
return options; //continue
}, onResponse: (Response response) async {
// Do something with response data
return response; // continue
}, onError: (DioError error) async {
// Do something with response error
if (error.response.statusCode == 401) {
Response response =
await dio.post("http://addrees-server/oauth/token",
options: Options(
headers: {
'Authorization': ApiUtils.BASIC_TOKEN,
'Content-Type': ApiUtils.CONTENT_TYPE,
},
),
queryParameters: {
"grant_type": ApiUtils.GRANT_TYPE,
"username": AppConstants.LOGIN,
"password": AppConstants.PASSWORD
});
Sessions.access_token = response.data['access_token'];
error.response.request.queryParameters
.update('access_token', (value) => Sessions.access_token);
RequestOptions options = error.response.request;
return dio.request(options.path, options: options); //continue
} else {
return error;
}
}));
client = RestClient(dio);
}

Angular2 Call login function from service in component and return error

I'm trying to call a service HTTP method and eventually return an error message but after a week of trying many things (Promises, Observables, ...) I can't get it to work. I hope anybody can help me out?
I'm kind of new to Angular2 and working alone on this project, with no one else around me with any Angular expertise. I did get a 3-day training course.
Component
#Component({
templateUrl: 'build/pages/login/login.html'
})
export class LoginPage {
error: string;
constructor(private navController: NavController, private auth: AuthService) {
}
private login(credentials) {
// Method calling the login service
// Could return an error, or nothing
this.error = this.auth.login(credentials);
// If there is no error and the user is set, go to other page
// This check is executed before previous login methode is finished...
if (!this.error && this.auth.user) {
this.navController.setRoot(OverviewPage);
}
}
}
AuthService
#Injectable()
export class AuthService {
private LOGIN_URL: string = "http://localhost:8080/rest/auth";
private USER_URL: string = "http://localhost:8080/rest/user";
private contentHeader: Headers = new Headers({
"Content-Type": "application/json"
});
errorMessage: string;
user: User;
constructor(private http: Http) {
}
login(credentials) {
let contentHeader = new Headers({
"Content-Type": "application/json"
});
this.http.post(this.LOGIN_URL, JSON.stringify(credentials), { headers: contentHeader })
.map(res => res.json())
.catch(this.handleError)
.subscribe(
data => this.handleLogin(data),
err => this.handleError
);
// could return an errorMessage or nothing/null
return this.errorMessage;
}
private handleLogin(data) {
let token = data.token;
this.getAccount(token);
}
private getAccount(token) {
let authHeader = new Headers({
"Content-Type": "application/json",
"X-Auth-Token": token
});
this.http.get(this.USER_URL, { headers: authHeader })
.map(res => res.json())
.catch(this.handleError)
.subscribe(
data => this.setUser(data),
err => this.errorMessage = err
);
}
private setUser(data) {
this.user = new User(data.naam, data.voornaam);
}
private handleError(error) {
// this.errorMessage is not saved?
if (error.status === 401) {
this.errorMessage = '401';
} else if (error.status === 404) {
this.errorMessage = '404';
} else {
this.errorMessage = 'Server error';
}
return Observable.throw(error.json() || 'Server error');
}
}
I think your problem is that your login method is returning a flat value (errorMessage). Since the login method is making an asynchronous request that value will not be initialized, it will always return null. If I were to set this up I would have the login method return an Observable.
Then to make things a bit more complicated it appears you want to make a consecutive call after login to get the logged in user. If you don't want your login method to emit until you've completed both calls you have to combine them somehow. I think switch can do this.
#Injectable()
export class AuthService {
private LOGIN_URL: string = "http://localhost:8080/rest/auth";
private USER_URL: string = "http://localhost:8080/rest/user";
private contentHeader: Headers = new Headers({
"Content-Type": "application/json"
});
user: User;
constructor(private http: Http) {
}
login(credentials) {
let contentHeader = new Headers({
"Content-Type": "application/json"
});
let response:Observable<Response> = this.http.post(this.LOGIN_URL, JSON.stringify(credentials), { headers: contentHeader });
//Take response and turn it into either a JSON object or
//a string error.
//This is an Observable<any> (any is returned by json())
let jsonResponse = response.map(res => res.json())
.catch(err => this.handleError(err));
//Take JSON object and turn it into an Observable of whatever the
//login request returns
//This is an Observable<Observable<any>> (Observable<any> is returned
//by handleLogin
let userResponse = jsonResponse.map(
data => this.handleLogin(data)
);
//Switch to the observable of the login request
//This is an Observable<any>, we will switch to the Observable<any>
//returned by handleLogin
let finalResponse = userResponse.switch();
//Hide actual response value from user. This will return an
//observable that will emit null on success and an error message
//on error
//Again, an Observable<any> since we're mapping to null
return finalResponse.map(res => null);
}
//We need to return this call as an observable so we can wire it into
//our chain
private handleLogin(data) {
let token = data.token;
return this.getAccount(token);
}
private getAccount(token) {
let authHeader = new Headers({
"Content-Type": "application/json",
"X-Auth-Token": token
});
let loginResponse = this.http.get(this.USER_URL, { headers: authHeader })
.map(res => res.json())
.catch((err) => this.handleError(err));
loginResponse.subscribe(
data => this.setUser(data)
);
return loginResponse;
}
private setUser(data) {
this.user = new User(data.naam, data.voornaam);
}
private handleError(error) {
let errorMessage = "Uninitialized";
if (error.status === 401) {
errorMessage = '401';
} else if (error.status === 404) {
errorMessage = '404';
} else {
errorMessage = error.json() || 'Server error';
}
return Observable.throw(errorMessage);
}
}
Now in your login component you will need to listen asynchronously to the response. This won't happen immediately (probably pretty quick with localhost, but may take a while in the real world) so I've added a loginDisabled that you can use to prevent the user from hitting the login button twice while waiting for the login request to be fulfilled.
#Component({
templateUrl: 'build/pages/login/login.html'
})
export class LoginPage {
error: string;
loginDisabled:boolean = false;
constructor(private navController: NavController, private auth: AuthService) {
}
private login(credentials) {
// Method calling the login service
// Could return an error, or nothing
this.loginDisabled = true;
this.auth.login(credentials).subscribe(
rsp => {
//On success, navigate to overview page
this.navController.setRoot(OverviewPage);
}, err => {
//On failure, display error message
this.error = err;
this.loginDisabled = false;
});
}
}
No promises this is all correct (I don't have anything to test it against) but it should be the right general direction.

How to use Soap Header with AngularJS AJAX

I create application for mobile use ionic framework connect to sql server 2008 through ASP.NET webservice. When I use SOAP header AngularJs $http.post() does not send data.
Login Method
public AuthHeader Authentication;
[SoapHeader("Authentication", Required = true)]
[WebMethod(Description = "Returns login User")]
public string Login(string Username, string Password)
{
if (Authentication.Username.ToLower() != "admin" ||
Authentication.Password.ToLower() != "admin#123")
throw new SoapException("Unauthorized",
SoapException.ClientFaultCode);
Password = Encrypt.GetSHA1HashData(Password);
var result = EmployeeService.Login(Username, Password);
if (result == null)
return "-1";
return JsonConvert.SerializeObject(result);
}
Angular Service
.service('LoginService', function ($q, $http) {
return {
loginUser: function (name, pw) {
var deferred = $q.defer();
var promise = deferred.promise;
$http.post('http://192.168.187.1/SalesServices/MobileService.asmx/Login',
{
Username: 'thao.huynh',
Password: 'thao.huynh'
}).then(function (response) {
deferred.resolve(response.data);
// console.log(response.data);
}).catch(function (response) {
deferred.reject('error', response.status, response.data);
console.error('error', response.status, response.data);
}).finally(function () {
// console.log("finally finished gists");
});
promise.success = function (fn) {
promise.then(fn);
return promise;
}
promise.error = function (fn) {
promise.then(null, fn);
return promise;
}
return promise;
}
}
For this you can refer this answer...
It shows the simplest way to call a service using soap
https://stackoverflow.com/a/11404133/3156647

Resources