Authentication - AppDaddy-Software-Solutions-Inc/framework-markup-language GitHub Wiki
FML core uses OAuth 2.0 authentication and JSON Web Tokens (JWT). To learn more about JWT you can visit https://jwt.io/.
JSON Web Token (JWT, RFC 7519) is a way to encode claims in a JSON document that is then signed. JWTs can be used as OAuth 2.0 Bearer Tokens to encode all relevant parts of an access token into the access token itself instead of having to store them in a database.
JWT's are created on the server after the user has successfully authenticated. The valid JWT needs to be returned to the client in the response "Authorization" header.
After each external call, FML looks to see if there is a bearer "Authentication" token in the response header. If one is exists, that token replaces the last and is used in future calls. This is useful when the server wishes to validate the tokens expiry date (age) on each call.
Additionally, each and every "claim" that is encoded in the JWT becomes accessible to the FML client as a bindable by the same name. For example, if the claim in included name and age, those values could be accessed in any template simply by binding to {USER.name}
and {USER.age}
. Additionally, {USER.connected}
is set to true and {USER.rights}
is set to the rights value passed as part of the claims list. If not supplied this value defaults to 0. Claims "connected" and "rights" are special values and are always set.
Page access authorization is performed by using the FML template header attribute "rights". If the "rights" attribute is not specified, the page is public access and does not require the user to be authenticated. If rights is specified, for example <TEMPLATE rights="16" />
the user must be authenticated and must have rights at or above level 16. If the user has not yet been authenticated, the template specified in key="LOGIN_PAGE"
in CONFIG.xml is automatically loaded.
After authentication, if the user rights are below those specified in the template, the template specified by key="UNAUTHORIZED_PAGE"
in CONFIG.xml is displayed.
Subsequent calls from FML will send the last received JWT in the request header as a bearer "Authorization" token message. This allows session-less API end points to authenticate and secure calls without the need to pass sensitive information.
If the server needs to renew the token, a new token should be generated and placed in the response headers as an "Authorization" bearer message. Please note, some browsers such as Google Chrome and Microsoft Edge require the server to explicitly allow CORS response headers. This can be achieved by setting the Access-Control-Expose-Headers custom header to the appropriate value. In the example below, its set to allow "all" requestors.
On ISS, the web.config file might look like this:
<customHeaders>
<add name="Access-Control-Allow-Origin" value="*" />
<add name="Access-Control-Allow-Headers" value="*" />
<add name="Access-Control-Allow-Methods" value="*" />
<add name="Access-Control-Expose-Headers" value="*" />
</customHeaders>
When the FML page raises the LOGOFF()
event, all bindable user "claims" are cleared, {USER.connected} is set to false and {USER.rights} is set to null.
Here is an example written in C# .NET CORE that shows the how to create a JWT:
public String encodeJwt(Dictionary<String, String> claims = null)
{
String encoded;
try
{
byte[] privateKey = Convert.FromBase64String(this.privateKey);
using RSA rsa = RSA.Create();
rsa.ImportRSAPrivateKey(privateKey, out _);
var signingCredentials = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256)
{
CryptoProviderFactory = new CryptoProviderFactory { CacheSignatureProviders = false }
};
var now = DateTime.Now;
List<Claim> _claims = new List<Claim>();
if (claims != null)
foreach (var claim in claims) if (claim.Key != null) _claims.Add(new Claim(claim.Key, claim.Value ?? ""));
_claims.Add(new Claim(JwtRegisteredClaimNames.Iat, Converters.toString(new DateTimeOffset(now).ToUnixTimeSeconds()), ClaimValueTypes.Integer64));
_claims.Add(new Claim(JwtRegisteredClaimNames.Jti, Converters.toString(Guid.NewGuid())));
var jwt = new JwtSecurityToken(
audience: audience,
issuer: issuer,
claims: _claims,
notBefore: now,
expires: now.AddMinutes(shelflife),
signingCredentials: signingCredentials
);
encoded = new JwtSecurityTokenHandler().WriteToken(jwt);
return $"Bearer {encoded}";
}
catch (Exception ex)
{
exception = ex;
return null;
}
}
}
Note, the privateKey that is referenced in the above code, is a secure RSA key and is stored in the appsettings.json file on the server. An RSA key pair for your application can be generated by visiting https://cryptotools.net/rsagen.
#Third Party Authentication via Firebase
FML has built in 3rd party authentication via Google Firebase. In order use firebase authentication, you must first setup an account with Google Firebase. Google Firebase can be found here https://firebase.google.com/.
Once you have setup your Google Firebase account, you will need 2 things, the Google Firebase API key and your authenticating domain. Both of these values will need to be added to the FML config.xml file as follows:
<FIREBASE_API_KEY value="AIzaCyKkVjzo9jXXQZfc1xYsPokpNsgKnovWo3II"/>
<FIREBASE_AUTH_DOMAIN value="mydomain.firebaseapp.com"/>
The "logon" event includes a parameter called "provider". This value must match the name of the providers setup in your Firebase Console. For example "facebook", or "microsoft".
Raising the FML event logon(null, null, , "facebook") will launch the Facebook user authentication popup. The , if provided, is the id of the datasource that will be fired on successful authentication.
On successful authentication, the Jwt token returned from Firebase replaces the current Jwt token and is sent in the request "Authorization" header on subsequent datasource calls. Like local authorization described above, the Jwt claims returned from Firebase include email and any exposed properties in the Firebase "profile". These properties can be bound using {USER.}.
In many cases, the developer may want to authenticate using firebase, then validate the Firebase Jwt, access its claims and augment those claims with additional information from the database. This is done server side by detecting the source of the Jwt token. In the case of Firebase, the Jwt source
Here is an example written in C# .NET CORE that shows the how to read a Jwt token:
private bool read(String token)
{
bool ok = true;
try
{
if (handler == null) handler = new JwtSecurityTokenHandler();
var jwt = handler.ReadToken(token) as JwtSecurityToken;
var kid = jwt.Header.ContainsKey("kid") ? (String)jwt.Header["kid"] : null;
var iss = jwt.Payload.ContainsKey("iss") ? (String)jwt.Payload["iss"] : null;
var url = $"{iss}/.well-known/openid-configuration";
if (!url.Contains("://")) url = $"https://{url}";
public Dictionary<String, String> claims = new Dictionary<string, string>();
foreach (var claim in jwt.Payload) claims[claim.Key] = Converters.toString(claim.Value) ?? "";
}
catch (Exception ex)
{
ok = false;
exception = ex;
}
return ok;
}
Here is an example written in C# .NET CORE that shows the how to read a Validate token issued from an external source:
private Exception validateToken(String token)
{
try
{
ConfigurationManager<OpenIdConnectConfiguration> configManager = new ConfigurationManager<OpenIdConnectConfiguration>(url, new OpenIdConnectConfigurationRetriever());
OpenIdConnectConfiguration config = configManager.GetConfigurationAsync().Result;
TokenValidationParameters validationParameters = new TokenValidationParameters
{
ValidateIssuer = validateIssuer,
ValidateAudience = validateAudience,
ValidateLifetime = validateLifetime,
ValidateIssuerSigningKey = validateSignature,
IssuerSigningKeys = config.SigningKeys,
};
handler.ValidateToken(token, validationParameters, out var validatedSecurityToken);
}
catch (Exception ex)
{
return ex;
}
return null;
}