Last week I am trying to configure the IdentityServer4 to get an access token automatically updating.
I had an API:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:5100";
options.RequireHttpsMetadata = false;
options.ApiName = "api1";
});
My MVC client configuration:
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = "http://localhost:5100";
options.RequireHttpsMetadata = false;
options.ClientId = "mvc";
options.ClientSecret = "secret";
options.ResponseType = "code id_token";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("api1");
options.Scope.Add("offline_access");
});
And the IdentityServer's clients configuration:
return new List<Client>
{
new Client
{
ClientId = "mvc",
ClientName = "My mvc",
AllowedGrantTypes = GrantTypes.Hybrid,
RequireConsent = false,
AccessTokenLifetime = 10,
ClientSecrets =
{
new Secret("secret".Sha256())
},
RedirectUris = { "http://localhost:5102/signin-oidc" },
PostLogoutRedirectUris = { "http://localhost:5102/signout-callback-oidc" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.OfflineAccess,
"api1"
},
AllowOfflineAccess = true
}
};
On the client side I use AJAX queries to call the API to get/post/put/delete data. I add the access token to the request and get the result.
private async getAuthenticationHeader(): Promise<any> {
return axios.get('/token').then((response: any) => {
return { headers: { Authorization: `Bearer ${response.data}` } };
});
}
async getAsync<T>(url: string): Promise<T> {
return this.httpClient
.get(url, await this.getAuthenticationHeader())
.then((response: any) => response.data as T)
.catch((err: Error) => {
console.error(err);
throw err;
});
}
The access token is provided by the MVC client method:
[HttpGet("token")]
public async Task<string> GetAccessTokenAsync()
{
return await HttpContext.GetTokenAsync("access_token");
}
It works fine. After access token expired I get 401 on the client side, so it would be great to have an opportunity to update access token automatically when it was expired.
According to a documentation I supposed, that It can be reached by setting AllowOfflineAccess to true and adding suitable scope "offline_access".
Maybe I don't understand the right flow of the access and refresh tokens usages. Can I do it automatically or it is impossible? I suppose, that we can use refresh tokens in out queries, but I don't understand how.
I've read a lot of SO answers and github issues but I am still confused. Could you help me to figure out?
After investigation and communicating in comments I've found the answer. Before every API call I get the expite time and according to the result update access_token or return existing:
[HttpGet("config/accesstoken")]
public async Task<string> GetOrUpdateAccessTokenAsync()
{
var accessToken = await HttpContext.GetTokenAsync("access_token");
var expiredDate = DateTime.Parse(await HttpContext.GetTokenAsync("expires_at"), null, DateTimeStyles.RoundtripKind);
if (!((expiredDate - DateTime.Now).TotalMinutes < 1))
{
return accessToken;
}
lock (LockObject)
{
if (_expiredAt.HasValue && !((_expiredAt.Value - DateTime.Now).TotalMinutes < 1))
{
return accessToken;
}
var response = DiscoveryClient.GetAsync(_identitySettings.Authority).Result;
if (response.IsError)
{
throw new Exception(response.Error);
}
var tokenClient = new TokenClient(response.TokenEndpoint, _identitySettings.Id, _identitySettings.Secret);
var refreshToken = HttpContext.GetTokenAsync("refresh_token").Result;
var tokenResult = tokenClient.RequestRefreshTokenAsync(refreshToken).Result;
if (tokenResult.IsError)
{
throw new Exception();
}
accessToken = tokenResult.AccessToken;
var idToken = HttpContext.GetTokenAsync("id_token").Result;
var tokens = new List<AuthenticationToken>
{
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.IdToken,
Value = idToken
},
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.AccessToken,
Value = accessToken
},
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.RefreshToken,
Value = tokenResult.RefreshToken
}
};
var expiredAt = DateTime.UtcNow.AddSeconds(tokenResult.ExpiresIn);
tokens.Add(new AuthenticationToken
{
Name = "expires_at",
Value = expiredAt.ToString("o", CultureInfo.InvariantCulture)
});
var info = HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme).Result;
info.Properties.StoreTokens(tokens);
HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, info.Principal, info.Properties).Wait();
_expiredAt = expiredAt.ToLocalTime();
}
return accessToken;
}
}
I call this method to get the access_token and add int to the API call headers:
private async getAuthenticationHeader(): Promise<any> {
return axios.get('config/accesstoken').then((response: any) => {
return { headers: { Authorization: `Bearer ${response.data}` } };
});
}
async getAsync<T>(url: string): Promise<T> {
return this.axios
.get(url, await this.getAuthenticationHeader())
.then((response: any) => response.data as T)
.catch((err: Error) => {
console.error(err);
throw err;
});
}
Double check locking were implemented to prevent simultamious async API calls try to change access_token at the same time. Optionally you can cashe you access_token into static variable or cache, it is up to you.
If you have any advices or alternatives it would be insteresting to discuss. Hope it helps someone.
There's 2 ways of doing this:
Client side - Handle the authentication and obtaining of the token on the client side using a lib like oidc-client-js. This has a feature that allows automatic renewal of the token via a prompt=none call to the authorize endpoint behind the scenes.
Refresh token - store this in your existing cookie and then use it to request a new access token as needed. In this mode your client side code doing the AJAX calls would need to be aware of token errors and automatically request a new token from the server whereby GetAccessTokenAsync() could use the refresh token to get a new access token.
Related
How do you properly configure custom challenge by sending an access_token from
cookie without a session?
const {
CognitoUserPool,
AuthenticationDetails,
CognitoUser
} = require('amazon-cognito-identity-js');
async function asyncAuthenticateUser(cognitoUser, cognitoAuthenticationDetails) {
return new Promise(function (resolve, reject) {
cognitoUser.initiateAuth(cognitoAuthenticationDetails, {
onSuccess: resolve,
onFailure: reject,
customChallenge: resolve
})
})
}
async function asyncCustomChallengeAnswer(cognitoUser, challengeResponse) {
return new Promise(function (resolve, reject) {
cognitoUser.sendCustomChallengeAnswer(challengeResponse, {
onSuccess: resolve,
onFailure: reject,
customChallenge: reject // We do not expect a second challenge
}
})
}
// omitted part of codes for brevity...
// We have tokens as cookie already that means a successful login previously succeeded
// but this login has probably been done from a different client with a different client_id
// We call the custom auth flow along with the token we have to get a new one for the current client_id
// For this to work we need to extract the username from the cookie
let tokenDecoded = jwt_decode(cookies.access_token);
let tokenUsername = tokenDecoded['username'];
var authenticationData = {
Username: tokenUsername,
AuthParameters: {
Username: tokenUsername,
}
};
var authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails(authenticationData);
var poolData = {
UserPoolId: process.env.AUTH_AMPLIFYIDENTITYBROKERAUTH_USERPOOLID,
ClientId: client_id
};
var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);
var userData = {
Username: tokenUsername,
Pool: userPool
};
var cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);
cognitoUser.setAuthenticationFlowType("CUSTOM_AUTH");
try {
// Initiate the custom flow
await asyncAuthenticateUser(cognitoUser, authenticationDetails);
// Answer the custom challenge by providing the token
var result = await asyncCustomChallengeAnswer(cognitoUser, cookies.access_token);
var encrypted_id_token = await encryptToken(result.getIdToken().getJwtToken());
var encrypted_access_token = await encryptToken(result.getAccessToken().getJwtToken());
var encrypted_refresh_token = await encryptToken(result.getRefreshToken().getToken());
params.Item.id_token = encrypted_id_token;
params.Item.access_token = encrypted_access_token;
params.Item.refresh_token = encrypted_refresh_token;
}
catch (error) {
console.log("Token swap fail, this may be a tentative of token stealing");
return { // Redirect to login page with forced pre-logout
statusCode: 302,
headers: {
Location: '/?client_id=' + client_id + '&redirect_uri=' + redirect_uri + '&authorization_code=' + authorizationCode + '&forceAuth=true' + insertStateIfAny(event),
}
};
}
I have this part of codes where it invoke the iniateAuth then send a custom challenge answer.
// Initiate the custom flow
await asyncAuthenticateUser(cognitoUser, authenticationDetails);
// Answer the custom challenge by providing the token
var result = await asyncCustomChallengeAnswer(cognitoUser, cookies.access_token);
This complains due to the request session being empty.
The below are the auth challenge for create.
function handler(event, context, callback) {
// This function does nothing, the challenge do not need to be prepared
// Verify challenge will just verify the token provided
event.response.publicChallengeParameters = {};
event.response.publicChallengeParameters.question = 'JustGimmeTheToken';
event.response.privateChallengeParameters = {};
event.response.privateChallengeParameters.answer = 'unknown';
event.response.challengeMetadata = 'TOKEN_CHALLENGE';
event.response.challengeResult = true;
callback(null, event);
}
This is for the define challenge.
function handler(event, context, callback) {
// This function define the sequence to obtain a valid token from a valid token of another client
if (event.request.session.length == 0) {
// This is the first invocation, we ask for a custom challenge (providing a valid token)
event.response.issueTokens = false;
event.response.failAuthentication = false;
event.response.challengeName = 'CUSTOM_CHALLENGE';
} else if (
event.request.session.length == 1 &&
event.request.session[0].challengeName == 'CUSTOM_CHALLENGE' &&
event.request.session[0].challengeResult == true
) {
// The custom challenge has been answered we retrun the token
event.response.issueTokens = true;
event.response.failAuthentication = false;
}
context.done(null, event);
}
It goes both in this auth challenge but never goes to the verify challenge where
the is the below
function handler(event, context, callback) {
const params = {
AccessToken: event.request.challengeAnswer
};
const userInfo = await cognito.getUser(params).promise();
if (userInfo.Username === event.username) {
event.response.answerCorrect = true;
} else {
// Someone tried to get a token of someone else
event.response.answerCorrect = false;
}
callback(null, event);
}
Here I want to achieve the SSO feature. if I logout from Identity server all clients connected to that with that userid should be logged out. But it is working for me.
I am using Identity Server Version="4.0.0"
I have setup the IDS clients , UI templates for loggedout, MVC client as below. But it is not signout from all clients.
new Client
{
ClientId = "testmvc",
AllowedGrantTypes = GrantTypes.Code,
// secret for authentication
ClientSecrets =
{
new Secret("Secret".Sha256())
},
AllowOfflineAccess = true,
// scopes that client has access to
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Address,
"roles"
},
// where to redirect to after login
RedirectUris = { "https://localhost:5002/signin-oidc" },
// where to redirect to after logout
PostLogoutRedirectUris = { "https://localhost:5002/signout-callback-oidc" },
FrontChannelLogoutUri = "https://localhost:5002/home/frontchannellogout" ,
}
In MVC client I have created two logouts
//UI Logout
public async Task<IActionResult> Logout()
{
var client = _httpClientFactory.CreateClient("IDPClient");
var discoveryDocumentResponse = await client.GetDiscoveryDocumentAsync();
if (discoveryDocumentResponse.IsError)
{
throw new Exception(discoveryDocumentResponse.Error);
}
var accessTokenRevocationResponse = await client.RevokeTokenAsync(
new TokenRevocationRequest
{
Address = discoveryDocumentResponse.RevocationEndpoint,
ClientId = "testmvc",
ClientSecret = "Secret",
Token = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken)
});
if (accessTokenRevocationResponse.IsError)
{
throw new Exception(accessTokenRevocationResponse.Error);
}
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
return Redirect(discoveryDocumentResponse.EndSessionEndpoint);
}
//Front channel logout
public async Task<IActionResult> FrontChannelLogout(string sid)
{
if (User.Identity.IsAuthenticated)
{
var currentSid = User.FindFirst("sid")?.Value ?? "";
if (string.Equals(currentSid, sid, StringComparison.Ordinal))
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
}
return NoContent();
}
In the Logout method, you are signing out from both auth schemes (Cookies and OIDC), whereas in FrontChannelLogout you seem to be signing out only from the Cookies scheme. Isn't that the problem?
Either way, please try to define the methods like below, and check if the corresponding OIDC logout endpoint is called.
public async Task Logout()
{
// ...
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
}
public async Task FrontChannelLogout(string sid)
{
if (User.Identity.IsAuthenticated)
{
//...
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
}
}
How can you define the auth provider? Now every time the auth variable is undefined in the playerLogin method.
I'm are using Adonis v4.1
The code:
start/socket.js
const Server = use('Server')
const io = use('socket.io')(Server.getInstance())
// Define controllers here
// Example: const WSController = use('App/Controllers/Http/ChatController')
const AuthenticateController = use('App/Controllers/Http/AuthenticateController');
io.on('connection', function (socket) {
// Define here the controller methods
// Example: WSController.goMessage(socket, io)
AuthenticateController.playerLogin(socket, io);
AuthenticateController.playerRegister(socket, io);
})
AuthenticateController.js
const Hash = use('Hash')
const User = use('App/Models/User')
class AuthenticateController {
static playerLogin(socket, io, {auth}) {
socket.on('playerLogin', async (data) => {
console.log('WORKS')
if (await auth.attempt(data.email, data.password)) {
let user = await User.findBy('email', data.email)
let accessToken = await auth.generate(user)
socket.emit('sendPlayerToken', { token: accessToken });
} else {
socket.emit('sendPlayerToken', { token: 'Credentials are incorrect' });
}
});
}
static playerRegister(socket, io) {
socket.on('playerRegister', async (data) => {
const safePassword = await Hash.make(data.password)
const user = new User()
user.username = data.username
user.email = data.email
user.password = safePassword
await user.save()
socket.emit('sendPlayerRegister', { success: true });
});
}
}
Kind regards,
Corné
As this discussion bellow:
https://github.com/adonisjs/core/discussions/2051?sort=new#discussioncomment-274111
You can use the API guard and authenticate using the code from the answer of M4gie.
You only need to install the crypto package with yarn add crypto and if you don't want to create the object to display the socket errors, just turn it into a string: throw new Error('SocketErrors:MissingParameter').
I'm trying to add JWT validation in my dot net core application. I've followed this link to understand JWT and able to generate a token by givings some values like this.
var token = new JwtSecurityToken(
issuer: issuer,
audience: aud,
claims: claims,
expires: expTime,
signingCredentials: creds
);
Edit: and to follow this answer, I've also added JwtBearerAuthentication middleware in my app by adding app.UseJwtBearerAuthentication(new JwtBearerOptions { /* options */ }) to Startup.Configure() method.
Now I'm stuck how could I pass this token inside HTTP header? I'm generating this token on Login but whats next? How could I get to know that JWT is added and working fine??
Any kind of help will be appreciated.
This is a runnable sample for bearer token authentication in ASP.NET Core.
How to achieve a bearer token authentication and authorization in ASP.NET Core
At back end, you can generate the token following this code:
[Route("api/[controller]")]
public class TokenAuthController : Controller
{
[HttpPost]
public string GetAuthToken(User user)
{
var existUser = UserStorage.Users.FirstOrDefault(u => u.Username == user.Username && u.Password == user.Password);
if (existUser != null)
{
var requestAt = DateTime.Now;
var expiresIn = requestAt + TokenAuthOption.ExpiresSpan;
var token = GenerateToken(existUser, expiresIn);
return JsonConvert.SerializeObject(new {
stateCode = 1,
requertAt = requestAt,
expiresIn = TokenAuthOption.ExpiresSpan.TotalSeconds,
accessToken = token
});
}
else
{
return JsonConvert.SerializeObject(new { stateCode = -1, errors = "Username or password is invalid" });
}
}
private string GenerateToken(User user, DateTime expires)
{
var handler = new JwtSecurityTokenHandler();
ClaimsIdentity identity = new ClaimsIdentity(
new GenericIdentity(user.Username, "TokenAuth"),
new[] {
new Claim("ID", user.ID.ToString())
}
);
var securityToken = handler.CreateToken(new SecurityTokenDescriptor
{
Issuer = TokenAuthOption.Issuer,
Audience = TokenAuthOption.Audience,
SigningCredentials = TokenAuthOption.SigningCredentials,
Subject = identity,
Expires = expires
});
return handler.WriteToken(securityToken);
}
}
In Startup.cs/ConfigureServices method
services.AddAuthorization(auth =>
{
auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser().Build());
});
And add this code in Configure method
app.UseJwtBearerAuthentication(new JwtBearerOptions {
TokenValidationParameters = new TokenValidationParameters {
IssuerSigningKey = TokenAuthOption.Key,
ValidAudience = TokenAuthOption.Audience,
ValidIssuer = TokenAuthOption.Issuer,
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(0)
}
});
At front end, you just add the token to header like this:
$.ajaxSetup({
headers: { "Authorization": "Bearer " + accessToken }
});
or
$.ajax("http://somedomain/somepath/somepage",{
headers:{ "Authorization": "Bearer " + accessToken },
/*some else parameter for ajax, see more you can review the Jquery API*/
});
I am using passport-facebook to login in a MEAN stack webapp. After successful login, I want to generate a JSON Web Token (jwt) and redirect to a page in my SPA. (res.redirect('/#/posts/'+ doc.generateJWT()); -- please see the associated code below).
My question is:
How do I send the JWT to the redirect page without showing it in the URL?
Code:
passport.serializeUser(function(user, done) {
done(null, user);
});
passport.deserializeUser(function(obj, done) {
done(null, obj);
});
passport.use(new FacebookStrategy({
clientID: FACEBOOK_APP_ID,
clientSecret: FACEBOOK_APP_SECRET,
callbackURL: FACEBOOK_CALLBACKURL
},
function(accessToken, refreshToken, profile, done) {
process.nextTick(function () {
User.findOne({'fbid':profile.id},function(err, docs) {
if (err){
//console.log('Error in SignUp: '+err);
return res.status(401).json(info);
}
else {
if (docs) {
//console.log('User already exists');
globalid = profile.id;
return done(null,docs);
} else {
// if there is no user with that fbid
// create the user
var newUser = new User();
// set the user's local credentials
newUser.fbid = profile.id;
globalid = profile.id;
newUser.firstname = profile.name.givenName;
newUser.lastname = profile.name.familyName;
newUser.gender = profile.gender;
if(profile.emails){
newUser.fbemail = profile.emails[0].value;
};
newUser.fblink = profile.profileUrl;
newUser.fbverified = profile.verified;
// save the user
newUser.save(function(err) {
if (err){
//console.log('Error in Saving user: '+err);
return res.status(401).json(info);
}
//console.log('User Registration succesful');
return done(null, newUser);
});
}
}
});
});
}));
var router = express.Router();
router.get('/auth/facebook',
passport.authenticate('facebook', { scope : 'email' }
));
router.get('/auth/facebook/callback',
passport.authenticate('facebook', { session: false, failureRedirect: '/'}),
function(req, res,done) {
var redirection = true;
User.findOne({ 'fbid': globalid }, function (err, doc){
//console.log("Generating token");
doc.token = doc.generateJWT();
doc.save(function(err) {
if (err){
//console.log('Error in Saving token for old user: '+err);
return res.status(401).json(info);
}
else
{
//console.log('User Login succesful');
redirection = doc.mobileverified;
//console.log(redirection);
//return done(null, doc);
if(doc.mobileverified === true){
console.log("Token:",doc.generateJWT());
res.redirect('/#/posts/'+ doc.generateJWT());
}
else{
console.log("Token:",doc.generateJWT());
//res.json({token: doc.generateJWT()});
res.redirect('/#/register/' + doc.generateJWT());
}
}
});
});
});
Many Thanks in advance!
If you don't wanna show your token on the url you have to send the response as json
var fbOptions = {
clientID: FACEBOOK_APP_ID,
clientSecret: FACEBOOK_APP_SECRET,
callbackURL: FACEBOOK_CALLBACKURL
};
passport.use(new FacebookStrategy(fbOptions, function(token, refreshToken, profile, done) {
var user = profile;
// NOTE: ‘my_token’ we will use later
user.my_token = 'generate your jwt token';
done(null, user);
}));
And then on your router return the token as json
app.get('/auth/facebook/callback', passport.authenticate('facebook', {session: false, failureRedirect : '/'}), function(req, res) {
// The token we have created on FacebookStrategy above
var token = req.user.my_token;
res.json({ token: token });
});