Create an OIDC credential Issuer with MATTR and ASP.NET Core | Software Engineer...
source link: https://damienbod.com/2021/05/03/create-an-oidc-credential-issuer-with-mattr-and-asp-net-core/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Create an OIDC credential Issuer with MATTR and ASP.NET Core
This article shows how to create and issue verifiable credentials using MATTR and an ASP.NET Core. The ASP.NET Core application allows an admin user to create an OIDC credential issuer using the MATTR service. The credentials are displayed in an ASP.NET Core Razor Page web UI as a QR code for the users of the application. The user can use a digital wallet form MATTR to scan the QR code, authenticate against an Auth0 identity provider configured for this flow and use the claims from the id token to add the verified credential to the digital wallet. In a follow up post, a second application will then use the verified credentials to allow access to a second business process.
Code: https://github.com/swiss-ssi-group/MattrGlobalAspNetCore
Blogs in the series
- Getting started with Self Sovereign Identity SSI
- Create an OIDC credential Issuer with MATTR and ASP.NET Core
Setup
The solutions involves an MATTR API which handles all the blockchain identity logic. An ASP.NET Core application is used to create the digital identity and the OIDC credential issuer using the MATTR APIs and also present this as a QR code which can be scanned. An identity provider is required to add the credential properties to the id token. The properties in a verified credential are issued using the claims values from the id token so a specific identity provider is required with every credential issuer using this technic. Part of the business of this solution is adding business claims to the identity provider. A MATTR digital wallet is required to scan the QR code, authenticate against the OIDC provider which in our case is Auth0 and then store the verified credentials to the wallet for later use.
MATTR Setup
You need to register with MATTR and create a new account. MATTR will issue you access to your sandbox domain and you will get access data from them plus a link to support.
Once setup, use the OIDC Bridge tutorial to implement the flow used in this demo. The docs are really good but you need to follow the docs exactly.
https://learn.mattr.global/tutorials/issue/oidc-bridge/issue-oidc
Auth0 Setup
A standard trusted web application which supports the code flow is required so that the MATTR digital wallet can authenticate using the identity provider and use the id token values from the claims which are required in the credential. It is important to create a new application which is only used for this because the client secret is required when creating the OIDC credential issuer and is shared with the MATTR platform. It would probably be better to use certificates instead of a shared secret which is persisted in different databases. We also use a second Auth0 application configuration to sign into the web application but this is not required to issue credentials.
In Auth0, rules are used to extend the id token claims. You need to add your claims as required by the MATTR API and your business logic for the credentials you wish to issue.
function
(user, context, callback) {
context.idToken[namespace +
'license_issued_at'
] = user.user_metadata.license_issued_at;
context.idToken[namespace +
'license_type'
] = user.user_metadata.license_type;
context.idToken[namespace +
'name'
] = user.user_metadata.name;
context.idToken[namespace +
'first_name'
] = user.user_metadata.first_name;
context.idToken[namespace +
'date_of_birth'
] = user.user_metadata.date_of_birth;
callback(
null
, user, context);
}
For every user (holder) who should be able to create verifiable credentials, you must add the credential data to the user profile. This is part of the business process with this flow. If you were to implement this for a real application with lots of users, it would probably be better to integrate the identity provider into the solution issuing the credentials and add a UI for editing the user profile data which is used in the credentials. This would be really easy using ASP.NET Core Identity and for example OpenIddict or IdentityServer4. It is important that the user cannot edit this data. This logic is part of the credential issuer logic and not part of the user profile.
After creating a new MATTR OIDC credential issuer, the callback URL needs to be added to the Open ID connect code flow client used for the digital wallet sign in.
Add the URL to the Allowed Callback URLs in the settings of your Auth0 application configuration for the digital wallet.
Implementing the OpenID Connect credentials Issuer application
The ASP.NET Core application is used to create new OIDC credential issuers and also display the QR code for these so that the verifiable credential can be loaded to the digital wallet. The application requires secrets. The data is stored to a database, so that any credential can be added to a wallet at a later date and also so that you can find the credentials you created. The MattrConfiguration is the data and the secrets you received from MATTR for you account access to the API. The Auth0 configuration is the data required to sign in to the application. The Auth0Wallet configuration is the data required to create the OIDC credential issuer so that the digital wallet can authenticate the identity using the Auth0 application. This data is stored in the user secrets during development.
{
// use user secrets
"ConnectionStrings"
: {
"DefaultConnection"
:
"--your-connection-string--"
},
"MattrConfiguration"
: {
"ClientId"
:
"--your-client-id--"
,
"ClientSecret"
:
"--your-client-secret--"
,
"TenantId"
:
"--your-tenant--"
,
"TenantSubdomain"
:
"--your-tenant-sub-domain--"
,
},
"Auth0"
: {
"Domain"
:
"--your-auth0-domain"
,
"ClientId"
:
"--your--auth0-client-id--"
,
"ClientSecret"
:
"--your-auth0-client-secret--"
,
}
"Auth0Wallet"
: {
"Domain"
:
"--your-auth0-wallet-domain"
,
"ClientId"
:
"--your--auth0-wallet-client-id--"
,
"ClientSecret"
:
"--your-auth0-wallet-client-secret--"
,
}
}
Accessing the MATTR APIs
The MattrConfiguration DTO is used to fetch the MATTR account data for the API access and to use in the application.
public
class
MattrConfiguration
{
public
string
Audience {
get
;
set
; }
public
string
ClientId {
get
;
set
; }
public
string
ClientSecret {
get
;
set
; }
public
string
TenantId {
get
;
set
; }
public
string
TenantSubdomain {
get
;
set
; }
public
string
Url {
get
;
set
; }
}
The MattrTokenApiService is used to acquire an access token and used for the MATTR API access. The token is stored to a cache and only fetched if the old one has expired or is not available.
public
class
MattrTokenApiService
{
private
readonly
ILogger<MattrTokenApiService> _logger;
private
readonly
MattrConfiguration _mattrConfiguration;
private
static
readonly
Object _lock =
new
Object();
private
IDistributedCache _cache;
private
const
int
cacheExpirationInDays = 1;
private
class
AccessTokenResult
{
public
string
AcessToken {
get
;
set
; } =
string
.Empty;
public
DateTime ExpiresIn {
get
;
set
; }
}
private
class
AccessTokenItem
{
public
string
access_token {
get
;
set
; } =
string
.Empty;
public
int
expires_in {
get
;
set
; }
public
string
token_type {
get
;
set
; }
public
string
scope {
get
;
set
; }
}
private
class
MattrCrendentials
{
public
string
audience {
get
;
set
; }
public
string
client_id {
get
;
set
; }
public
string
client_secret {
get
;
set
; }
public
string
grant_type {
get
;
set
; } =
"client_credentials"
;
}
public
MattrTokenApiService(
IOptions<MattrConfiguration> mattrConfiguration,
IHttpClientFactory httpClientFactory,
ILoggerFactory loggerFactory,
IDistributedCache cache)
{
_mattrConfiguration = mattrConfiguration.Value;
_logger = loggerFactory.CreateLogger<MattrTokenApiService>();
_cache = cache;
}
public
async
Task<
string
> GetApiToken(HttpClient client,
string
api_name)
{
var
accessToken = GetFromCache(api_name);
if
(accessToken !=
null
)
{
if
(accessToken.ExpiresIn > DateTime.UtcNow)
{
return
accessToken.AcessToken;
}
else
{
// remove => NOT Needed for this cache type
}
}
_logger.LogDebug($
"GetApiToken new from oauth server for {api_name}"
);
// add
var
newAccessToken =
await
GetApiTokenClient(client);
AddToCache(api_name, newAccessToken);
return
newAccessToken.AcessToken;
}
private
async
Task<AccessTokenResult> GetApiTokenClient(HttpClient client)
{
try
{
var
payload =
new
MattrCrendentials
{
client_id = _mattrConfiguration.ClientId,
client_secret = _mattrConfiguration.ClientSecret,
audience = _mattrConfiguration.Audience
};
var
tokenResponse =
await
client.PostAsJsonAsync(authUrl, payload);
if
(tokenResponse.StatusCode == System.Net.HttpStatusCode.OK)
{
var
result =
await
tokenResponse.Content.ReadFromJsonAsync<AccessTokenItem>();
DateTime expirationTime = DateTimeOffset.FromUnixTimeSeconds(result.expires_in).DateTime;
return
new
AccessTokenResult
{
AcessToken = result.access_token,
ExpiresIn = expirationTime
};
}
_logger.LogError($
"tokenResponse.IsError Status code: {tokenResponse.StatusCode}, Error: {tokenResponse.ReasonPhrase}"
);
throw
new
ApplicationException($
"Status code: {tokenResponse.StatusCode}, Error: {tokenResponse.ReasonPhrase}"
);
}
catch
(Exception e)
{
_logger.LogError($
"Exception {e}"
);
throw
new
ApplicationException($
"Exception {e}"
);
}
}
private
void
AddToCache(
string
key, AccessTokenResult accessTokenItem)
{
var
options =
new
DistributedCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromDays(cacheExpirationInDays));
lock
(_lock)
{
_cache.SetString(key, JsonConvert.SerializeObject(accessTokenItem), options);
}
}
private
AccessTokenResult GetFromCache(
string
key)
{
var
item = _cache.GetString(key);
if
(item !=
null
)
{
return
JsonConvert.DeserializeObject<AccessTokenResult>(item);
}
return
null
;
}
}
Generating the API DTOs using Nswag
The MattrOpenApiClientSevice file was generated using Nswag and the Open API file provided by MATTR here. We only generated the DTOs using this and access the client then using a HttpClient instance. The Open API file used in this solution is deployed in the git repo.
Creating the OIDC credential issuer
The MattrCredentialsService is used to create an OIDC credentials issuer using the MATTR APIs. This is implemented using the CreateCredentialsAndCallback method. The created callback is returned so that it can be displayed in the UI and copied to the specific Auth0 application configuration.
private
readonly
IConfiguration _configuration;
private
readonly
DriverLicenseCredentialsService _driverLicenseService;
private
readonly
IHttpClientFactory _clientFactory;
private
readonly
MattrTokenApiService _mattrTokenApiService;
private
readonly
MattrConfiguration _mattrConfiguration;
public
MattrCredentialsService(IConfiguration configuration,
DriverLicenseCredentialsService driverLicenseService,
IHttpClientFactory clientFactory,
IOptions<MattrConfiguration> mattrConfiguration,
MattrTokenApiService mattrTokenApiService)
{
_configuration = configuration;
_driverLicenseService = driverLicenseService;
_clientFactory = clientFactory;
_mattrTokenApiService = mattrTokenApiService;
_mattrConfiguration = mattrConfiguration.Value;
}
public
async
Task<
string
> CreateCredentialsAndCallback(
string
name)
{
// create a new one
var
driverLicenseCredentials =
await
CreateMattrDidAndCredentialIssuer();
driverLicenseCredentials.Name = name;
await
_driverLicenseService.CreateDriverLicense(driverLicenseCredentials);
var
callback = $
"https://{_mattrConfiguration.TenantSubdomain}/ext/oidc/v1/issuers/{driverLicenseCredentials.OidcIssuerId}/federated/callback"
;
return
callback;
}
The CreateMattrDidAndCredentialIssuer method implements the different steps described in the MATTR API documentation for this. An access token for the MATTR API is created or retrieved from the cache and DID is created and the id from the DID post response is used to create the OIDC credential issuer. The DriverLicenseCredentials is returned which is persisted to a database and the callback is created using this object.
private
async
Task<DriverLicenseCredentials> CreateMattrDidAndCredentialIssuer()
{
HttpClient client = _clientFactory.CreateClient();
var
accessToken =
await
_mattrTokenApiService
.GetApiToken(client,
"mattrAccessToken"
);
client.DefaultRequestHeaders.Authorization =
new
AuthenticationHeaderValue(
"Bearer"
, accessToken);
client.DefaultRequestHeaders
.TryAddWithoutValidation(
"Content-Type"
,
"application/json"
);
var
did =
await
CreateMattrDid(client);
var
oidcIssuer =
await
CreateMattrCredentialIssuer(client, did);
return
new
DriverLicenseCredentials
{
Name =
"not_named"
,
Did = JsonConvert.SerializeObject(did),
OidcIssuer = JsonConvert.SerializeObject(oidcIssuer),
OidcIssuerId = oidcIssuer.Id
};
}
The CreateMattrDid method creates a new DID as specified by the API. The MattrOptions class is used to create the request object. This is serialized using the StringContentWithoutCharset class due to a bug in the MATTR API validation. I created this class using the blog from Gunnar Peipman.
private
async
Task<V1_CreateDidResponse> CreateMattrDid(HttpClient client)
{
// create did , post to dids
var
payload =
new
MattrOpenApiClient.V1_CreateDidDocument
{
Method = MattrOpenApiClient.V1_CreateDidDocumentMethod.Key,
Options =
new
MattrOptions()
};
var
payloadJson = JsonConvert.SerializeObject(payload);
var
uri =
new
Uri(createDidUrl);
using
(
var
content =
new
StringContentWithoutCharset(payloadJson,
"application/json"
))
{
var
createDidResponse =
await
client.PostAsync(uri, content);
if
(createDidResponse.StatusCode == System.Net.HttpStatusCode.Created)
{
var
v1CreateDidResponse = JsonConvert.DeserializeObject<V1_CreateDidResponse>(
await
createDidResponse.Content.ReadAsStringAsync());
return
v1CreateDidResponse;
}
var
error =
await
createDidResponse.Content.ReadAsStringAsync();
}
return
null
;
}
The MattrOptions DTO is used to create a default DID using the key type “ed25519”. See the MATTR API docs for further details.
public
class
MattrOptions
{
/// <summary>
/// The supported key types for the DIDs are ed25519 and bls12381g2.
/// If the keyType is omitted, the default key type that will be used is ed25519.
///
/// If the keyType in options is set to bls12381g2 a DID will be created with
/// a BLS key type which supports BBS+ signatures for issuing ZKP-enabled credentials.
/// </summary>
public
string
keyType {
get
;
set
; } =
"ed25519"
;
}
The CreateMattrCredentialIssuer implements the OIDC credential issuer to create the post request. The request properties need to be setup for your credential properties and must match claims from the id token of the Auth0 user profile. This is where the OIDC client for the digital wallet is setup and also where the credential claims are specified. If this is setup up incorrectly, loading the data into your wallet will fail. The HTTP request and the response DTOs are implemented using the Nswag generated classes.
private
async
Task<V1_CreateOidcIssuerResponse> CreateMattrCredentialIssuer(HttpClient client, V1_CreateDidResponse did)
{
// create vc, post to credentials api
var
payload =
new
MattrOpenApiClient.V1_CreateOidcIssuerRequest
{
Credential =
new
Credential
{
IssuerDid = did.Did,
Name =
"NationalDrivingLicense"
,
Context =
new
List<Uri> {
},
Type =
new
List<
string
> {
"nationaldrivinglicense"
}
},
ClaimMappings =
new
List<ClaimMappings>
{
new
ClaimMappings{ JsonLdTerm=
"name"
, OidcClaim=$
"https://{_mattrConfiguration.TenantSubdomain}/name"
},
new
ClaimMappings{ JsonLdTerm=
"firstName"
, OidcClaim=$
"https://{_mattrConfiguration.TenantSubdomain}/first_name"
},
new
ClaimMappings{ JsonLdTerm=
"licenseType"
, OidcClaim=$
"https://{_mattrConfiguration.TenantSubdomain}/license_type"
},
new
ClaimMappings{ JsonLdTerm=
"dateOfBirth"
, OidcClaim=$
"https://{_mattrConfiguration.TenantSubdomain}/date_of_birth"
},
new
ClaimMappings{ JsonLdTerm=
"licenseIssuedAt"
, OidcClaim=$
"https://{_mattrConfiguration.TenantSubdomain}/license_issued_at"
}
},
FederatedProvider =
new
FederatedProvider
{
ClientId = _configuration[
"Auth0Wallet:ClientId"
],
ClientSecret = _configuration[
"Auth0Wallet:ClientSecret"
],
Scope =
new
List<
string
> {
"openid"
,
"profile"
,
"email"
}
}
};
var
payloadJson = JsonConvert.SerializeObject(payload);
var
uri =
new
Uri(createCredentialsUrl);
using
(
var
content =
new
StringContentWithoutCharset(payloadJson,
"application/json"
))
{
var
createOidcIssuerResponse =
await
client.PostAsync(uri, content);
if
(createOidcIssuerResponse.StatusCode == System.Net.HttpStatusCode.Created)
{
var
v1CreateOidcIssuerResponse = JsonConvert.DeserializeObject<V1_CreateOidcIssuerResponse>(
await
createOidcIssuerResponse.Content.ReadAsStringAsync());
return
v1CreateOidcIssuerResponse;
}
var
error =
await
createOidcIssuerResponse.Content.ReadAsStringAsync();
}
throw
new
Exception(
"whoops something went wrong"
);
}
Now the service is completely ready to generate credentials. This can be used in any Blazor UI, Razor page or MVC view in ASP.NET Core. The services are added to the DI in the startup class. The callback method is displayed in the UI if the application successfully creates a new OIDC credential issuer.
private
readonly
MattrCredentialsService _mattrCredentialsService;
public
bool
CreatingDriverLicense {
get
;
set
; } =
true
;
public
string
Callback {
get
;
set
; }
[BindProperty]
public
IssuerCredential IssuerCredential {
get
;
set
; }
public
AdminModel(MattrCredentialsService mattrCredentialsService)
{
_mattrCredentialsService = mattrCredentialsService;
}
public
void
OnGet()
{
IssuerCredential =
new
IssuerCredential();
}
public
async
Task<IActionResult> OnPostAsync()
{
if
(!ModelState.IsValid)
{
return
Page();
}
Callback =
await
_mattrCredentialsService
.CreateCredentialsAndCallback(IssuerCredential.CredentialName);
CreatingDriverLicense =
false
;
return
Page();
}
}
public
class
IssuerCredential
{
[Required]
public
string
CredentialName {
get
;
set
; }
}
Adding credentials you wallet
After the callback method has been added to the Auth0 callback URLs, the credentials can be used to add verifiable credentials to your wallet. This is fairly simple. The Razor Page uses the data from the database and generates an URL using the MATTR specification and the id from the created OIDC credential issuer. The claims from the id token or the profile data is just used to display the data for the user signed into the web application. This is not the same data which is used be the digital wallet. If the same person logs into the digital wallet, then the data is the same. The wallet authenticates the identity separately.
public
class
DriverLicenseCredentialsModel : PageModel
{
private
readonly
DriverLicenseCredentialsService _driverLicenseCredentialsService;
private
readonly
MattrConfiguration _mattrConfiguration;
public
string
DriverLicenseMessage {
get
;
set
; } =
"Loading credentials"
;
public
bool
HasDriverLicense {
get
;
set
; } =
false
;
public
DriverLicense DriverLicense {
get
;
set
; }
public
string
CredentialOfferUrl {
get
;
set
; }
public
DriverLicenseCredentialsModel(DriverLicenseCredentialsService driverLicenseCredentialsService,
IOptions<MattrConfiguration> mattrConfiguration)
{
_driverLicenseCredentialsService = driverLicenseCredentialsService;
_mattrConfiguration = mattrConfiguration.Value;
}
public
async
Task OnGetAsync()
{
//"license_issued_at": "2021-03-02",
//"license_type": "B1",
//"name": "Bob",
//"first_name": "Lammy",
//"date_of_birth": "1953-07-21"
var
identityHasDriverLicenseClaims =
true
;
var
nameClaim = User.Claims.FirstOrDefault(t => t.Type == $
"https://{_mattrConfiguration.TenantSubdomain}/name"
);
var
firstNameClaim = User.Claims.FirstOrDefault(t => t.Type == $
"https://{_mattrConfiguration.TenantSubdomain}/first_name"
);
var
licenseTypeClaim = User.Claims.FirstOrDefault(t => t.Type == $
"https://{_mattrConfiguration.TenantSubdomain}/license_type"
);
var
dateOfBirthClaim = User.Claims.FirstOrDefault(t => t.Type == $
"https://{_mattrConfiguration.TenantSubdomain}/date_of_birth"
);
var
licenseIssuedAtClaim = User.Claims.FirstOrDefault(t => t.Type == $
"https://{_mattrConfiguration.TenantSubdomain}/license_issued_at"
);
if
(nameClaim ==
null
|| firstNameClaim ==
null
|| licenseTypeClaim ==
null
|| dateOfBirthClaim ==
null
|| licenseIssuedAtClaim ==
null
)
{
identityHasDriverLicenseClaims =
false
;
}
if
(identityHasDriverLicenseClaims)
{
DriverLicense =
new
DriverLicense
{
Name = nameClaim.Value,
FirstName = firstNameClaim.Value,
LicenseType = licenseTypeClaim.Value,
DateOfBirth = dateOfBirthClaim.Value,
IssuedAt = licenseIssuedAtClaim.Value,
UserName = User.Identity.Name
};
// get per name
//var offerUrl = await _driverLicenseCredentialsService.GetDriverLicenseCredentialIssuerUrl("ndlseven");
// get the last one
var
offerUrl =
await
_driverLicenseCredentialsService.GetLastDriverLicenseCredentialIssuerUrl();
DriverLicenseMessage =
"Add your driver license credentials to your wallet"
;
CredentialOfferUrl = offerUrl;
HasDriverLicense =
true
;
}
else
{
DriverLicenseMessage =
"You have no valid driver license"
;
}
}
}
The data is displayed using Bootstrap. If you use a MATTR wallet to scan the QR Code shown underneath, you will be redirected to authenticate against the specified Auth0 application. If you have the claims, you can add verifiable claims to you digital wallet.
Notes
MATTR API has a some problems with its API and a stricter validation would help a lot. But MATTR support is awesome and the team are really helpful and you will end up with a working solution. It would be also awesome if the Open API file could be used without changes to generate a client and the DTOs. It would makes sense, if you could issue credentials data from the data in the credential issuer application and not from the id token of the user profile. I understand that in some use cases, you would like to protect against any wallet taking credentials for other identities, but I as a credential issuer cannot always add my business data to user profiles from the IDP. The security of this solution all depends on the user profile data. If a non authorized person can change this data (in this case, this could be the same user), then incorrect verifiable credentials can be created.
Next step is to create an application to verify and use the verifiable credentials created here.
Links
https://mattr.global/get-started/
https://learn.mattr.global/tutorials/dids/did-key
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK