This article pairs with another article: “Technical Review of Civic’s Secure Identity Platform”. The verdict of which is that the current implementation has some bizarre design decisions that do not add anything to the overall security. Instead, a standardized approach should have been taken using OAuth or OpenID Connect, as opposed to the current self-rolled authentication protocol. I would not recommend you use Civic.
To get started with civic, I’m going to use it as an authentication method in an ASP.NET Core application. This will use the ASP.NET Core MVC Visual Studio template, with no authentication. Authentication is going to be triggered manually using a login button in the sites header.
You can find the completed proof of concept on GitHub.
Getting the Authorization Code from Civic
The first part of the authentication process with Civic is to get the authorization code from Civic. This involves generating the authorization request, transferring the request to the user (e.g. via QR code), and then polling Civic for authorization (i.e. repeatedly asking Civic: “have they authorized me yet?”).
I don’t really want to waste time here trying to display QR codes, so I’m going to use Civic’s JavaScript library to do this first step for us. That being said, I see no reason that the authorization code request cannot be generated server side, and also the polling done server side. Only the QR code needs to be sent to the user agent (browser). This way we could keep our authorization code off of the user agent.
For now, let’s stick with what Civic give us and add the following libraries to our _Layout.cshtml:
At the end of the head:
<link rel="stylesheet" href="https://hosted-sip.civic.com/css/civic-modal.min.css">
At the end of the body:
<script src="https://hosted-sip.civic.com/js/civic.sip.min.js"></script>
For the sake of this demo, I’m using any page to load up the login screen. A better approach would be to only use this library on pages not including any other JavaScript libraries, in order to minimise the attack surface.
Next, lets add a button to our navbar start the authentication request:
<li><button id="civicLogin" class="btn btn-link navbar-btn">Login using Civic</button></li>
Now we need to do something on button click, where appId is the client ID that was created for us in the integration portal:
<script>
var civicSip = new civic.sip({ appId: 'rJ3fVI9rz' });
var button = document.querySelector('#civicLogin');
button.addEventListener('click', function () {
civicSip.signup({ style: 'popup', scopeRequest: civicSip.ScopeRequests.BASIC_SIGNUP });
});
</script>
This enables our QR code popup, displayed using an iFrame. If we take a look at this QR code it is the following URL style (decoded with line breaks added for readability):
https://api.civic.com/sip/prod/scopeRequest/5963e82b-1602-491c-abf6-a619706dea6c/callback
?appId=rJ3fVI9rz
&callbackUrl=https://api.civic.com/sip/prod/scopeRequest/5963e82b-1602-491c-abf6-a619706dea6c/callback
&expires=1517740951.844
Which returns JSON that looks like:
{
"clientId": "rJ3fVI9rz",
"callbackUrl": "https://api.civic.com/sip/prod/scopeRequest/5963e82b-1602-491c-abf6-a619706dea6c/callback",
"verificationLevel": "civicBasic",
"name": "Test App",
"type": "test",
"logo": "https://hosted-sip.civic.com/scopeRequest/prod/img/logo-in-testing.png",
"primaryColor": "A80B00",
"secondaryColor": "FFFFFF",
"description": "$name would like to access the following data on your identity",
"partnerUrl": "http://localhost:5000/",
"validationMethod": "jwtToken",
"jwtToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJqdGkiOiI5MWNkMGRjNS0xMDQ1LTQzNTQtYjIyOS1lMDUzMTczZTIzMDQiLCJpYXQiOjE1MTc3MzkyMDguNDk2LCJleHAiOjE1MTc3NDEwMDguNDk2LCJpc3MiOiJjaXZpYy1zaXAtcGFydG5lci1zZXJ2aWNlIiwiYXVkIjoiaHR0cHM6Ly9hcGkuY2l2aWMuY29tL3NpcC8iLCJzdWIiOiJySjNmVkk5cnoiLCJkYXRhIjp7ImNsaWVudElkIjoickozZlZJOXJ6IiwiY2FsbGJhY2tVcmwiOiJodHRwczovL2FwaS5jaXZpYy5jb20vc2lwL3Byb2Qvc2NvcGVSZXF1ZXN0LzU5NjNlODJiLTE2MDItNDkxYy1hYmY2LWE2MTk3MDZkZWE2Yy9jYWxsYmFjayIsInZlcmlmaWNhdGlvbkxldmVsIjoiY2l2aWNCYXNpYyIsIm5hbWUiOiJUZXN0IEFwcCIsInR5cGUiOiJ0ZXN0IiwibG9nbyI6Imh0dHBzOi8vaG9zdGVkLXNpcC5jaXZpYy5jb20vc2NvcGVSZXF1ZXN0L3Byb2QvaW1nL2xvZ28taW4tdGVzdGluZy5wbmciLCJwcmltYXJ5Q29sb3IiOiJBODBCMDAiLCJzZWNvbmRhcnlDb2xvciI6IkZGRkZGRiIsImRlc2NyaXB0aW9uIjoiJG5hbWUgd291bGQgbGlrZSB0byBhY2Nlc3MgdGhlIGZvbGxvd2luZyBkYXRhIG9uIHlvdXIgaWRlbnRpdHkiLCJwYXJ0bmVyVXJsIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwLyJ9fQ.HV4a1FgSJY81ID3LNBr-dyloEu-mgyxNPzlIYer1goESa2PgatRjv4oOw2QQnpKe1S0tdYkqHpb5UTai3g_gZw"
}
Where jwtToken
is the same data, but repeated in a signed JWT.
We can then scan this QR code using our Civic app and authorize the data request.
Whilst this is happening, our browser is polling Civic for the result on https://api.civic.com/sip/prod/scopeRequest/5963e82b-1602-491c-abf6-a619706dea6c
If the user hasn’t authorized the app yet, then a 202 (Accepted) is returned. Once they do authorize the app, a 200 (OK) is received along with the following data:
{
"authResponse": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI1ZTBmNWU4Ni0wNTBmLTRiNzQtYjcwNy0wODFjODBiOTYxZWYiLCJpYXQiOjE1MTcxMzUzMjQuNjA4LCJleHAiOjE1MTcxMzcxMjQuNjA4LCJpc3MiOiJjaXZpYy1zaXAtaG9zdGVkLXNlcnZpY2UiLCJhdWQiOiJodHRwczovL2FwaS5jaXZpYy5jb20vc2lwLyIsInN1YiI6InJKM2ZWSTlyeiIsImRhdGEiOnsiY29kZVRva2VuIjoiOTBjNWZlZjQtNmY4ZC00Yzg2LWFjMGQtMzk0YjI3ODNiZDFkIn19.OSMCu-bORoaFzLT-eVp4EjcHq9bj6Z38hf9PcWyXvoLiky7pONmTxRppeTQRFWdlndaNFArO_5t_R6-tVbDydQ",
"statusCode": 200,
"action": "data-received",
"type": "code"
}
The authResponse is another JWT:
{
"alg": "ES256",
"typ": "JWT"
}
{
"jti": "5e0f5e86-050f-4b74-b707-081c80b961ef",
"iat": 1517135324.608,
"exp": 1517137124.608,
"iss": "civic-sip-hosted-service",
"aud": "https://api.civic.com/sip/",
"sub": "rJ3fVI9rz",
"data": {
"codeToken": "90c5fef4-6f8d-4c86-ac0d-394b2783bd1d"
}
}
We can receive this from the civic library using the following event on the civic library:
civicSip.on('auth-code-received', function (event) {
var jwtToken = event.response;
});
Now that we have the auth code, we need to send it up to the server to securely validate it and exchange it for identity data. First let’s send it to the server using AJAX:
$.ajax({
url: 'home/login',
dataType: 'json',
type: 'post',
contentType: 'application/json',
data: JSON.stringify({ "authCode": jwtToken })
});
Then we’ll say, upon success, reload the page:
sucess: function() {
location.reload();
}
And now we need a corresponding login action on our home controller. Let’s stub that out like so:
[HttpPost]
public async Task<IActionResult> Login([FromBody] LoginModel model) {
if (string.IsNullOrWhiteSpace(model?.AuthCode)) return BadRequest();
// TODO: Our logic will go here
return Ok();
}
public class LoginModel {
public string AuthCode { get; set; }
}
Exchanging the Authorization Code for User Data
Now that we are server side, we need to exchange the authCode for user data. Civic are responsible for validating the authCode we received (they are the audience in the JWT, therefore it is meant for them) and then returning the user data we sent from the Civic App.
To do this we have to:
-
Create a signed (ES256) JWT containing the following (nested within a “data” claim)
- method = POST
- path = scopeRequest/authCode
- Create a hash (HMAC SHA256) of the Authorization Code (but now called the authToken)
- Use an authorization header with the JWT and hash concatenated with a period, with a scheme of “Civic”
- POST to the Civic API with the authToken in the body
Creating the JWT
JWT’s need to be signed using ECDSA using P-256 curve and SHA-256 hash algorithm (aka ES256). So, let’s bring in the following nuget packages so that we can do this:
install-package Portable.BouncyCastle
install-package System.IdentityModel.Tokens.Jwt
First, we need to convert the hexadecimal string into an instance of ECDsa. The best approach I came up with is detailed in full in my article JWT Signing using ECDSA in .NET Core. So, before we begin let’s create a helper method for converting a hexadecimal string into a byte array:
private static byte[] FromHexString(string hex) {
var numberChars = hex.Length;
var hexAsBytes = new byte[numberChars / 2];
for (var i = 0; i < numberChars; i += 2)
hexAsBytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
return hexAsBytes;
}
And now we can get our instance of ECDsa
:
// your private signing key from Civic
const string privateKey = "e8593ad98db1dda0f57c16ef1f53c4c6b57fa35d9b5f82602353ccfb5a71f047";
var privKeyInt = new Org.BouncyCastle.Math.BigInteger(+1, FromHexString(privateKey));
var parameters = SecNamedCurves.GetByName("secp256r1");
var qa = parameters.G.Multiply(privKeyInt);
var privKeyX = qa.Normalize().XCoord.ToBigInteger().ToByteArrayUnsigned();
var privKeyY = qa.Normalize().YCoord.ToBigInteger().ToByteArrayUnsigned();
var privateKeyEcdsa = ECDsa.Create(new ECParameters {
Curve = ECCurve.NamedCurves.nistP256,
D = privKeyInt.ToByteArrayUnsigned(),
Q = new ECPoint {
X = privKeyX,
Y = privKeyY
}
});
And then cobble together our JWT:
var now = DateTime.UtcNow;
var tokenHandler = new JwtSecurityTokenHandler();
var signingCredentials = new SigningCredentials(new ECDsaSecurityKey(privateKeyEcdsa), SecurityAlgorithms.EcdsaSha256);
var jwtHeader = new JwtHeader(signingCredentials);
var jwtPayload = new JwtPayload(
issuer: "rJ3fVI9rz",
audience: "https://api.civic.com/sip",
claims: new List<Claim> {
new Claim("sub", "rJ3fVI9rz"),
new Claim("jti", Guid.NewGuid().ToString())
},
notBefore: null,
expires: now.AddMinutes(5),
issuedAt: now);
jwtPayload.Add("data", new Dictionary<string, string> { { "method", "POST" }, { "path", "scopeRequest/authCode" } });
var exchangeToken = tokenHandler.WriteToken(new JwtSecurityToken(jwtHeader, jwtPayload));
We haven’t taken advantage of the usual CreateJwtSecurityToken
method here, because of the random nested data value.
The best way I could figure out was to split things into the JwtHeader
and JwtPayload
objects.
Creating the Message Digest
Now that we have our token, we need to create our POST body, and then create a HMAC SHA256 message digest of the body using our secret. We then need to base64 encode this hash ready for transport.
// your secret from Civic
const string secret = "482283244e2e8082d6f1c3ef288930ce";
var data = JsonConvert.SerializeObject(new { authToken = model.AuthCode });
var secretBytes = Encoding.Default.GetBytes(secret);
var dataBytes = Encoding.Default.GetBytes(data);
var hmac = new HMACSHA256(secretBytes);
var hashAsBytes = hmac.ComputeHash(dataBytes);
var hash = Convert.ToBase64String(hashAsBytes);
Requesting User Data
Now we have everything we need to create our authorization header and make a request to the Civic API authCode endpoint.
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Civic", $"{exchangeToken}.{hash}");
var responseMessage = await httpClient.PostAsync("https://api.civic.com/sip/prod/scopeRequest/authCode", new StringContent(data, Encoding.UTF8, "application/json"));
var response = await responseMessage.Content.ReadAsStringAsync();
The response is JSON containing:
- data: our (usually) encrypted user data
- userId
- encrypted (boolean)
- alg (aes)
Decrypting the Response
Verifying the JWT Signature
With the response in hand we can now verify the JWT to ensure that it hasn’t been tampered with. To do this we need to use the public key for the private key that Civic used to signed the JWT with. The only way I was able to find this key was by looking through the existing Civic libraries…
const string civicAuthServerPublicKey = "049a45998638cfb3c4b211d72030d9ae8329a242db63bfb0076a54e7647370a8ac5708b57af6065805d5a6be72332620932dbb35e8d318fce18e7c980a0eb26aa1";
var pubKeyX = FromHexString(civicAuthServerPublicKey).Skip(1).Take(32).ToArray();
var pubKeyY = FromHexString(civicAuthServerPublicKey).Skip(33).ToArray();
var publicKeyEcdsa = ECDsa.Create(new ECParameters {
Curve = ECCurve.CreateFromFriendlyName("secp256r1"),
Q = new ECPoint {
X = pubKeyX,
Y = pubKeyY
}
});
var jObject = JObject.Parse(response);
var jToken = jObject["data"];
var jwt = jToken.Value<string>();
var claimsPrincipal = tokenHandler.ValidateToken(jwt, new TokenValidationParameters {
ValidIssuer = "civic-sip-hosted-service",
ValidateIssuer = true,
ValidAudience = "https://api.civic.com/sip/",
ValidateAudience = true,
ValidateLifetime = true,
IssuerSigningKey = new ECDsaSecurityKey(publicKeyEcdsa)
}, out var _);
Decrypting the data
Now we can decrypt data:
var loadedData = claimsPrincipal.FindFirst("data").Value;
var iv = FromHexString(loadedData.Substring(0, 32));
var encryptedData = Convert.FromBase64String(loadedData.Substring(32));
string plainTextUserData;
var aes = Aes.Create();
aes.IV = iv;
aes.Key = FromHexString(secret);
using (aes)
using (var memoryStream = new MemoryStream(encryptedData))
using (var cryptoStream = new CryptoStream(memoryStream, aes.CreateDecryptor(aes.Key, aes.IV), CryptoStreamMode.Read))
using (var srDecrypt = new StreamReader(cryptoStream))
plainTextUserData = srDecrypt.ReadToEnd();
Let’s create an object to parse this user data into:
public class UserData {
public string Label { get; set; }
public string Value { get; set; }
public bool IsValid { get; set; }
public bool IsOwner { get; set; }
}
And now deserialize our decrypted data:
var userData = JsonConvert.DeserializeObject<List<UserData>>(plainTextUserData);
Authenticating the User
The final piece of the puzzle is to log the user in.
For this I’m going to use the usual ASP.NET Core cookie middleware.
So, in our Startup.cs
, we add the following to the end of our
services.AddAuthentication("cookie")
.AddCookie("cookie");
And then in our Configure
method, before UseMvc
:
app.UseAuthentication();
And finally, at the end of our Login action:
var claimsIdentity = new ClaimsIdentity(userData.Select(x => new Claim(x.Label, x.Value)).ToList(), "cookie");
claimsIdentity.AddClaim(new Claim("userId", jObject["userId"].Value<string>()));
await HttpContext.SignInAsync("cookie", new ClaimsPrincipal(claimsIdentity));
return Ok();
Phew, done!
If you don't log into the ASP.NET Core site straight away, try refreshing the page. I don't get JavaScript sometimes...
Source Code and Release
Currently, this is one, great big method. If you’re interested in seeing the above turned into an ASP.NET Core authentication middleware, let me know in the comments. Or, feel free to use the above code to do it yourself (the above is licensed under Creative Commons Attribution 4.0 International).
A working solution for the above can be found on GitHub, just be sure to change the app ID, private key & secret to your own.