I am writing a Flutter/Dart application and am getting a JWT back from an auth server that has some claims I need to use. I have looked at various (4 so far) Dart JWT libraries -- but all are either too old and no longer work with Dart 2, etc. or they need the secret to decode the JWT which makes no sense and isn't correct (or possible since I have no access ).
So -- how can one get a JWT and get the claims from it within a "modern" Dart/Flutter application?
JWT tokens are just base64 encoded JSON strings (3 of them, separated by dots):
import 'dart:convert';
Map<String, dynamic> parseJwt(String token) {
final parts = token.split('.');
if (parts.length != 3) {
throw Exception('invalid token');
}
final payload = _decodeBase64(parts[1]);
final payloadMap = json.decode(payload);
if (payloadMap is! Map<String, dynamic>) {
throw Exception('invalid payload');
}
return payloadMap;
}
String _decodeBase64(String str) {
String output = str.replaceAll('-', '+').replaceAll('_', '/');
switch (output.length % 4) {
case 0:
break;
case 2:
output += '==';
break;
case 3:
output += '=';
break;
default:
throw Exception('Illegal base64url string!"');
}
return utf8.decode(base64Url.decode(output));
}
Use 'base64Url.normalize()' function.
That's what _decodeBase64() does from the answer above!
String getJsonFromJWT(String splittedToken){
String normalizedSource = base64Url.normalize(encodedStr);
return utf8.decode(base64Url.decode(normalizedSource));
}
As of this writing, the jaguar_jwt package is being actively maintained. Although it is not clearly documented, it does have a public method that will decode Base64Url encoding. It does basically the same thing as the accepted answer.
//import 'package:jaguar_jwt/jaguar_jwt.dart';
final String token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NTQ4MjAxNjIsImlhdCI6MTU1NDc3Njk2MiwiaXNzIjoiU3VyYWdjaCIsInN1YiI6IjQifQ.bg5B_k9WCmxiu2epuZo_Tpt_KZC4N9ve_2GEdrulcXM';
final parts = token.split('.');
final payload = parts[1];
final String decoded = B64urlEncRfc7515.decodeUtf8(payload);
This gives a JSON string, which for this particular example is:
{
"exp":1554820162,
"iat":1554776962,
"iss":"Suragch",
"sub":"4"
}
See also:
JWT: The Complete Guide to JSON Web Tokens
String based data encoding: Base64 vs Base64url
you can use jwt_decoder package to decode and/or check if you token is expired
//to get claims from your token
main () {
String yourToken = "Your JWT";
Map<String, dynamic> decodedToken =
JwtDecoder.decode(yourToken);
/*
If the token has a valid format, you will get a
Map<String,dynamic> Your decoded token can look like:
{
"sub": "1234567890",
"name": "Gustavo",
"iat": 1516239022,
"exp": 1516239022,
"randomKey": "something else"
}
*/
}
//check if your token is expired
main () {
String yourToken = "Your JWT";
bool hasExpired = JwtDecoder.isExpired(yourToken);
// You will get a true / false response
// true: if the token is already expired
// false: if the token is not expired
}
you can get your token expiration date using
main () {
String yourToken = "Your JWT";
DateTime expirationDate = JwtDecoder.getExpirationDate(token);
// 2025-01-13 13:04:18.000
print(expirationDate);
}
you can also find out how old your token is
// Token payload must include an 'iat' field
main () {
String yourToken = "Your JWT";
Duration tokenTime = JwtDecoder.getTokenTime(token);
// 15
print(tokenTime.inDays);
}
to learn more about what JWT Decoder can do, visit their package documentation page
you can decode JWT base64 by seperate first part of it that contains "."
String decodeUserData(String code) {
String normalizedSource = base64Url.normalize(code.split(".")[1]);
return utf8.decode(base64Url.decode(normalizedSource));
}
Related
I have created a service account and downloaded my JSON Credential on Google Cloud Platform. I need to make REST POST call in .NET to DialogFlow Service API. At this moment, I can do it only with a generated token in PowerShell. Since, I need to do it all from script, I need to generate a JWT to pass as my bearer in my REST call. My Problem is that the generated JWT is not honored by Google.
I get my response in PowerShell based on this doc page and I replicate sample codes from this doc page to create my JWT.
public static string GetSignedJwt(string emailClient, string
dialogueFlowServiceApi, string privateKeyId, string privateKey, string
jsonPath)
{
// to get unix time in seconds
var unixTimeSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
// start time of Unix system
var origin = new DateTime(1970, 1, 1, 0, 0, 0, 0);
// adding milliseconds to reach the current time, it will be used for issueAt time
var nowDataTime = origin.AddSeconds(unixTimeSeconds);
// one hour after the current time, it will be used for expiration time
var oneHourFromNow = nowDataTime.AddSeconds(3600);
// holder of signed json web token that we will return at the end
var signedJwt = "";
try
{
// create our payload for Jwt
var payload = new Dictionary<string, object>
{
{"iss", emailClient},
{"sub", emailClient},
{"aud", dialogueFlowServiceApi},
{"iat", nowDataTime},
{"exp", oneHourFromNow}
};
// create our additional headers
var extraHeaders = new Dictionary<string, object>
{
{"kid", privateKeyId}
};
IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
IJsonSerializer serializer = new JsonNetSerializer();
IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder);
signedJwt = encoder.Encode(extraHeaders, payload, privateKey);
}
catch (Exception e)
{
Console.WriteLine(e);
// return null if there has been any error
return null;
}
finally
{
Console.WriteLine(signedJwt);
}
return signedJwt;
}
Notice that, it is needed to be signed in RSA256 by passing public and private keys, as Google did it in Java sample snippet, however, my equivalent in .Net gives me only Object reference not set to an instance of an object when I use that algorithm:
var key = RSA.Create(privateKey);
IJwtAlgorithm algorithm = new RS256Algorithm(null, key);
IJsonSerializer serializer = new JsonNetSerializer();
IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder);
signedJwt = encoder.Encode(extraHeaders, payload, privateKey);
Besides of correct keys, I am using https://dialogflow.googleapis.com/google.cloud.dialogflow.v2beta1.Intents as dialogFlow service API key.
I expect it that my generated JWT gets accepted, however it is rejected by Google.
1) You are using the wrong algorithm
Change this line of code:
IJwtAlgorithm algorithm = new RS256Algorithm(null, key);
To this:
IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
2) For the JWT headers:
var additional_headers = new Dictionary<string, object>
{
{ "kid", privateKeyId },
{ "alg", "RS256" },
{ "typ", "JWT" }
};
3) Your JWT Payload does not include a scope. I am not sure which scope you need but here is an example. Add this to the payload before creating the JWT:
string scope = "https://www.googleapis.com/auth/cloud-platform";
var payload = new Dictionary<string, object>
{
{"scope", scope},
{"iss", emailClient},
{"sub", emailClient},
{"aud", dialogueFlowServiceApi},
{"iat", nowDataTime},
{"exp", oneHourFromNow}
};
4) For most Google APIs (not all) you also need to exchange the Signed JWT for a Google OAuth Access Token:
public static string AuthorizeToken(string token, string auth_url)
{
var client = new WebClient();
client.Encoding = Encoding.UTF8;
var content = new NameValueCollection();
// Request a "Bearer" access token
content["assertion"] = token;
content["grant_type"] = "urn:ietf:params:oauth:grant-type:jwt-bearer";
var response = client.UploadValues(auth_url, "POST", content);
return Encoding.UTF8.GetString(response);
}
The Authorization URL for above:
string auth_url = "https://www.googleapis.com/oauth2/v4/token";
I work on an MS Teams app with a connection to MS Graph.
I'm trying to get an access token for MS Graph in MS Teams. To get a token I'm using MSAL js.
If I run the App with gulp serve I receive a valid token and I have access to the MS Graph endpoints. But if I build the app and install it in MS Teams the function userAgentApplication.acquireTokenSilent(config) is never executed. I tested it with a console.log before and after the call. There is no error thrown.
Do you have any idea why the above snippet is not executed in MS Teams (app and webapp)?
NEW:
On Home:
export function login() {
const url = window.location.origin + '/login.html';
microsoftTeams.authentication.authenticate({
url: url,
width: 600,
height: 535,
successCallback: function(result: string) {
console.log('Login succeeded: ' + result);
let data = localStorage.getItem(result) || '';
localStorage.removeItem(result);
let tokenResult = JSON.parse(data);
storeToken(tokenResult.accessToken)
},
failureCallback: function(reason) {
console.log('Login failed: ' + reason);
}
});
}
On Login
microsoftTeams.initialize();
// Get the tab context, and use the information to navigate to Azure AD login page
microsoftTeams.getContext(function (context) {
// Generate random state string and store it, so we can verify it in the callback
let state = _guid();
localStorage.setItem("simple.state", state);
localStorage.removeItem("simple.error");
// See https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols-implicit
// for documentation on these query parameters
let queryParams = {
client_id: "XXX",
response_type: "id_token token",
response_mode: "fragment",
scope: "https://graph.microsoft.com/User.Read openid",
redirect_uri: window.location.origin + "/login-end.html",
nonce: _guid(),
state: state,
login_hint: context.loginHint,
};
// Go to the AzureAD authorization endpoint (tenant-specific endpoint, not "common")
// For guest users, we want an access token for the tenant we are currently in, not the home tenant of the guest.
let authorizeEndpoint = `https://login.microsoftonline.com/${context.tid}/oauth2/v2.0/authorize?` + toQueryString(queryParams);
window.location.assign(authorizeEndpoint);
});
// Build query string from map of query parameter
function toQueryString(queryParams) {
let encodedQueryParams = [];
for (let key in queryParams) {
encodedQueryParams.push(key + "=" + encodeURIComponent(queryParams[key]));
}
return encodedQueryParams.join("&");
}
// Converts decimal to hex equivalent
// (From ADAL.js: https://github.com/AzureAD/azure-activedirectory-library-for-js/blob/dev/lib/adal.js)
function _decimalToHex(number) {
var hex = number.toString(16);
while (hex.length < 2) {
hex = '0' + hex;
}
return hex;
}
// Generates RFC4122 version 4 guid (128 bits)
// (From ADAL.js: https://github.com/AzureAD/azure-activedirectory-library-for-js/blob/dev/lib/adal.js)
function _guid() {...}
on login end
microsoftTeams.initialize();
localStorage.removeItem("simple.error");
let hashParams = getHashParameters();
if (hashParams["error"]) {
// Authentication/authorization failed
localStorage.setItem("simple.error", JSON.stringify(hashParams));
microsoftTeams.authentication.notifyFailure(hashParams["error"]);
} else if (hashParams["access_token"]) {
// Get the stored state parameter and compare with incoming state
let expectedState = localStorage.getItem("simple.state");
if (expectedState !== hashParams["state"]) {
// State does not match, report error
localStorage.setItem("simple.error", JSON.stringify(hashParams));
microsoftTeams.authentication.notifyFailure("StateDoesNotMatch");
} else {
// Success -- return token information to the parent page.
// Use localStorage to avoid passing the token via notifySuccess; instead we send the item key.
let key = "simple.result";
localStorage.setItem(key, JSON.stringify({
idToken: hashParams["id_token"],
accessToken: hashParams["access_token"],
tokenType: hashParams["token_type"],
expiresIn: hashParams["expires_in"]
}));
microsoftTeams.authentication.notifySuccess(key);
}
} else {
// Unexpected condition: hash does not contain error or access_token parameter
localStorage.setItem("simple.error", JSON.stringify(hashParams));
microsoftTeams.authentication.notifyFailure("UnexpectedFailure");
}
// Parse hash parameters into key-value pairs
function getHashParameters() {
let hashParams = {};
location.hash.substr(1).split("&").forEach(function (item) {
let s = item.split("="),
k = s[0],
v = s[1] && decodeURIComponent(s[1]);
hashParams[k] = v;
});
return hashParams;
}
Even though my answer is quite late, but I faced the same problem recently.
The solution is farely simple: MSALs silent login does not work in MSTeams (yet) as MSTeams relies on an IFrame Approach that is not supported by MSAL.
You can read all about it in this Github Issue
Fortunately, they are about to release a fix for this in Version msal 1.2.0 and there is already an npm-installable beta which should make this work:
npm install msal#1.2.0-beta.2
Update: I tried this myself - and the beta does not work as well. This was confirmed by Microsoft in my own Github Issue.
So I guess at the time being, you simply can't use MSAL for MS Teams.
I'm currently trying to access a Web API in Flutter that requires a JWT access token for authorization. The access token expires after a certain amount of time.
A new access token can be requested with a separate refresh token. Right now this access token refresh is performed as soon as a request returns a 401 response. After that, the failed request should be retried with the new access token.
I'm having trouble with this last step. It seems like a http.BaseRequest can only be sent once. How would I retry the http request with the new token?
As suggested in the dart http readme, I created a subclass of http.BaseClient to add the authorization behavior. Here is a simplified version:
import 'dart:async';
import 'package:http/http.dart' as http;
class AuthorizedClient extends http.BaseClient {
AuthorizedClient(this._authService) : _inner = http.Client();
final http.Client _inner;
final AuthService _authService;
Future<http.StreamedResponse> send(http.BaseRequest request) async {
final token = await _authService.getAccessToken();
request.headers['Authorization'] = 'Bearer $token';
final response = await _inner.send(request);
if (response.statusCode == 401) {
final newToken = await _authService.refreshAccessToken();
request.headers['Authorization'] = 'Bearer $newToken';
// throws error: Bad state: Can't finalize a finalized Request
final retryResponse = await _inner.send(request);
return retryResponse;
}
return response;
}
}
abstract class AuthService {
Future<String> getAccessToken();
Future<String> refreshAccessToken();
}
Here is what I came up with so far, based on Richard Heap's answer: To resend a request, we have to copy it.
So far I was not able to come up for a solution for stream requests!
http.BaseRequest _copyRequest(http.BaseRequest request) {
http.BaseRequest requestCopy;
if(request is http.Request) {
requestCopy = http.Request(request.method, request.url)
..encoding = request.encoding
..bodyBytes = request.bodyBytes;
}
else if(request is http.MultipartRequest) {
requestCopy = http.MultipartRequest(request.method, request.url)
..fields.addAll(request.fields)
..files.addAll(request.files);
}
else if(request is http.StreamedRequest) {
throw Exception('copying streamed requests is not supported');
}
else {
throw Exception('request type is unknown, cannot copy');
}
requestCopy
..persistentConnection = request.persistentConnection
..followRedirects = request.followRedirects
..maxRedirects = request.maxRedirects
..headers.addAll(request.headers);
return requestCopy;
}
You can't send the same BaseRequest twice. Make a new BaseRequest from the first one, and send that copy.
Here's some code (from io_client) to 'clone' a BaseRequest.
var copyRequest = await _inner.openUrl(request.method, request.url);
copyRequest
..followRedirects = request.followRedirects
..maxRedirects = request.maxRedirects
..contentLength = request.contentLength == null
? -1
: request.contentLength
..persistentConnection = request.persistentConnection;
request.headers.forEach((name, value) {
copyRequest.headers.set(name, value);
});
When i use authorization in headers for rest assured by bassing token variable giving "File Not Found" error. And if hardcode the token in headers then it's working successfully. Can anyone help me out what can be the issue. Below is my code,
public static void getIndentId() throws IOException {
if(indentId == null){
Payloads pal = new Payloads();
RestAssured.baseURI = CommonFunctions.getSitApiGatewayEndPoint();
String sessionKey = GbPortalLogin.getSessionKey();
String token = "Token "+sessionKey;
System.out.println(token);
Response res = given().
header("Content-Type","application/json").
header("Authorization",token).
body(pal.createIndentPayload()).
when().post(Resources.createNbIndent).
then().extract().response();
String s = res.asString();
System.out.println(s);
/*System.out.println(res.getBody().toString());
JsonPath js = CommonFunctions.rawToJson(res);
indentId = js.get("id").toString();*/
}
I have managed to get back a JWT token from Identity Server using OAuth2 and would like to extract the claims from the token.
When I use a token decoder such as https://developers.google.com/wallet/digital/docs/jwtdecoder, I can peek inside the token and it looks fine.
However I am not sure what decrypting to use in c# in order to use the Microsoft JwtSecurityTokenHandler.ValidateToken to get back a claims identity.
In identity server, I am using a symmetric key which I have pasted for reference in my code. The JWT token is also valid.
Would really appreciate some help:
string token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vaWRlbnRpdHlzZXJ2ZXIudjIudGhpbmt0ZWN0dXJlLmNvbS90cnVzdC9jaGFuZ2V0aGlzIiwiYXVkIjoidXJuOndlYmFwaXNlY3VyaXR5IiwibmJmIjoxMzk3MTEzMDY5LCJleHAiOjEzOTcxNDkwNjksIm5hbWVpZCI6InN0ZWZhbiIsInVuaXF1ZV9uYW1lIjoic3RlZmFuIiwiYXV0aG1ldGhvZCI6Ik9BdXRoMiIsImF1dGhfdGltZSI6IjIwMTQtMDQtMTBUMDY6NTc6NDguODEyWiIsImh0dHA6Ly9pZGVudGl0eXNlcnZlci50aGlua3RlY3R1cmUuY29tL2NsYWltcy9jbGllbnQiOiJyZWx5aW5nIHBhcnR5IDMgdGVzdCBjbGllbnQgbmFtZSIsImh0dHA6Ly9pZGVudGl0eXNlcnZlci50aGlua3RlY3R1cmUuY29tL2NsYWltcy9zY29wZSI6InVybjp3ZWJhcGlzZWN1cml0eSJ9.cFnmgHxrpy2rMg8B6AupVrJwltu7RhBAeIx_D3pxJeI";
string key = "ZHfUES/6wG28LY+SaMtvaeek34t2PBrAiBxur6MAI/w=";
var validationParameters = new TokenValidationParameters()
{
AllowedAudience = "urn:webapisecurity",
SigningToken = new ????
ValidIssuer = #"http://identityserver.v2.thinktecture.com/trust/changethis"
};
var tokenHandler = new JwtSecurityTokenHandler();
var principal = tokenHandler.ValidateToken(token, validationParameters);
What sort of SigningToken should I use for the validationParameters.SigningToken ??
You can use the following website to Decode the token
http://jwt.io/
or here is a code to Decode JWT Token using C#
class Program
{
static void Main(string[] args)
{
string token ="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vaWRlbnRpdHlzZXJ2ZXIudjIudGhpbmt0ZWN0dXJlLmNvbS90cnVzdC9jaGFuZ2V0aGlzIiwiYXVkIjoidXJuOndlYmFwaXNlY3VyaXR5IiwibmJmIjoxMzk3MTEzMDY5LCJleHAiOjEzOTcxNDkwNjksIm5hbWVpZCI6InN0ZWZhbiIsInVuaXF1ZV9uYW1lIjoic3RlZmFuIiwiYXV0aG1ldGhvZCI6Ik9BdXRoMiIsImF1dGhfdGltZSI6IjIwMTQtMDQtMTBUMDY6NTc6NDguODEyWiIsImh0dHA6Ly9pZGVudGl0eXNlcnZlci50aGlua3RlY3R1cmUuY29tL2NsYWltcy9jbGllbnQiOiJyZWx5aW5nIHBhcnR5IDMgdGVzdCBjbGllbnQgbmFtZSIsImh0dHA6Ly9pZGVudGl0eXNlcnZlci50aGlua3RlY3R1cmUuY29tL2NsYWltcy9zY29wZSI6InVybjp3ZWJhcGlzZWN1cml0eSJ9.cFnmgHxrpy2rMg8B6AupVrJwltu7RhBAeIx_D3pxJeI";
var parts = token.Split('.');
string partToConvert = parts[1];
var partAsBytes = Convert.FromBase64String(partToConvert);
var partAsUTF8String = Encoding.UTF8.GetString(partAsBytes, 0, partAsBytes.Count());
//JSON.net required
var jwt = JObject.Parse(partAsUTF8String);
Console.Write(jwt.ToString());
Console.ReadLine();
}
}
It's a BinarySecretSecurityToken - base64 decode the stringified key to use it.