10Duke Scale supports identity-based licensing for users: users authenticate themselves and every operation tracks the users' identity. For authentication, 10Duke Scale uses Open ID Connect (OIDC), an authentication framework built on OAuth 2.0. To use identity-based licensing, you need an OIDC-based identity provider service. If you don't have one configured, contact 10Duke sales for options.
The 10Duke Scale SDK for Java provides two types of OIDC authentication: browser-based authentication and OAuth 2.0 "Device
Authorization Grant" authentication. Device authentication is primarily intended for scenarios where the device lacks a browser: the user authenticates out-of-band using a browser on another platform. For browser-based authentication, the client uses default OS-browser to perform the login.
Note the following limitations:
- 10Duke Scale SDK for Java supports OIDC ID tokens for API call authorization, which are signed with RSA keys using RS256. For detailed description of ID token requirements, see 10Duke Scale API documentation.
- 10Duke Scale SDK for Java supports the following OAuth flows:
- "Proof Key for Code Exchange by OAuth Public Clients" (see RFC 7636).
- "OAuth 2.0 Device Authorization Grant" (see RFC 8628)
Browser-based authentication
10Duke Scale SDK for Java provides browser-based OIDC authentication using:
- operating system default browser
- a caller defined browser
"Loopback interface redirection" is used as part of the user authentication flow.
Workflow description
The client application opens a browser and starts the login flow with the OIDC provider. The client simultaneously opens a local HTTP server used to detect when the login is complete. After completing the login, the OIDC server issues an HTTP redirect to a preconfigured URL (the redirect URI) in the browser. For details, see RFC 8252: 7.3 Loopback Interface Redirection.
Configuration
The client must be configured with an HTTP message served by the HTTP server after the login is complete. The HTTP message is an HTTP response body (response message entity). The client application must also configure the OIDC provider's JSON Web Key Set URL and the OpenID Connect discovery URL, which advertises the OIDC provider's authorization settings.
Device authentication
If the device running the client lacks a browser, or using the browser is awkward, authentication can be performed using the Device Authorization Grant (or device flow).
Workflow description
To start the user login, the device shows an authentication URL and a user code to the user. Using another device (for example, a computer or tablet), the user starts authentication by navigating to the given URL and entering the user code. Once the authentication is complete, the client is notified and a user session is set up.
For details of the Device Authorization Grant, see RFC 8628.
When authenticating with device flow, the application developer must implement the display of the relevant information for the end user.
Configuration
The client application must configure the OIDC provider's JSON Web Key Set URL and the OpenID Connect discovery URL, which advertises the OIDC provider's authorization settings.
Examples
The process of implementing identity-based license consumption is explained in the 10Duke Scale documentation.
Desktop application example
First, let's look at the main program of this example, a Java CLI application that implements the following flow:
- initialize the application and 10Duke Scale SDK
- sign in the user using the OAuth PKCE flow
- query information to enable selecting a license to check out
- check out a seat
- release the checkout
public static void main(final String[] args)
throws IOException, InterruptedException, URISyntaxException {
if (!Desktop.isDesktopSupported()) {
throw new UnsupportedOperationException(
"Java AWT Desktop is not supported,"
+ " cannot run user sign-in");
}
CliLicensingExample example = new CliLicensingExample();
example.initClientApp();
example.signin();
System.out.println("waiting for login to complete...");
example.loginSemaphore.acquire();
example.selectLicensee();
if (example.selectedLicensee == null) {
System.out.println("No licensee selected, bye bye...");
return;
}
example.selectLicense();
if (example.selectedLicense == null) {
System.out.println("No license selected, bye bye...");
return;
}
LicenseToken licenseToken = example.checkoutLicense();
System.out.println(
"License checkout made for product"
+ licenseToken.getProductName()
+ " with status: "
+ licenseToken.getStatus());
LicenseReleaseResult releaseResult
= example.releaseLicense(licenseToken);
if (releaseResult != null) {
System.out.println(
"License release made for product"
+ releaseResult.getProductName()
+ " with status: "
+ (releaseResult.getErrorCode() == null
? " success"
: releaseResult.getErrorCode()));
}
System.out.println("All done, bye bye...");
}
Initialize the client application to use the 10Duke Scale SDK. The following example shows how to set up a client application to use the OAuth PKCE flow to sign in users. The example uses the DesktopAppClientBuilder client application builder utility.
private void initClientApp() throws IOException, URISyntaxException {
String configResource = "tenduke.conf";
String userAgent
= "tenduke-scale-sdk-java-cli-example/1.0.0";
clientBuilder = new DesktopAppClientBuilder(
configResource,
userAgent);
try {
client = clientBuilder.initForIdentityBasedLicensing();
} catch (SdkException ex) {
Logger
.getLogger("CliLicensingExample")
.log(
Level.SEVERE,
"""
Startup error, tip: check config file
and that APIs can be reached""",
ex);
throw ex;
}
}
This example shows signing in the user using the OAuth PKCE flow. Running the PKCE flow automatically updates the SDK's authorization provider used for setting the HTTP Authorization header for upcoming license checkout calls. See more about application configuration in: set up an application to use the 10Duke Scale SDK for Javaset up an application to use the 10Duke Scale SDK for Java. The call to signin() triggers an asynchronous workflow where the user authenticates via a browser. When the authentication with the identity provider completes, control returns to the authCodeCallback(final String code, final String error) method. The authorization code is then exchanged for an access token. The resulting ID token is used to print a welcome message and serves as the means to authorize calls to the 10Duke Scale licensing API.
private void signin() throws IOException {
lastPkceInfo = client.getPkceClient().authorize(
(url) -> openUrl(url),
(code, err) -> authCodeCallback(code, err),
false,
30);
}
Query which licensees the user is associated with.
private void selectLicensee() {
List<Licensee> licensees = client
.getLicenseCheckoutClient()
.describeLicenseConsumerLicensees(
null,
0,
10,
null,
true);
if (licensees.isEmpty()) {
System.out.println(
"""
No licensee relationships found --> \
no available licenses""");
return;
}
selectLicensee(licensees);
}
Query information on available licenses, based on having selected a licensee in the previous step.
private void selectLicense() {
availableLicenses = client
.getLicenseCheckoutClient()
.describeLicenseConsumerLicenses(
selectedLicensee.getId(),
null,
null,
null,
false,
0,
25,
"productName",
true);
selectedLicense = selectLicense(availableLicenses.getLicenses());
}
Check out a license based on selection.
private LicenseToken checkoutLicense() {
long qty = 1;
if (!QuantityDimension.SEATS.equals(
selectedLicense.getQtyDimension())) {
System.out.println(
"Enter quantity to consume,"
+ " mind that license has:"
+ selectedLicense.getQty()
+ " remaining.");
qty = scanner.nextLong();
}
Map<String, String> claims = new HashMap<>();
claims.put("cliHwId", hwId());
LicenseCheckoutArguments checkoutArguments
= LicenseCheckoutArguments
.builder()
.productName(
selectedLicense.getProductName())
.licenseId(
selectedLicense.getId())
.qty(qty)
.qtyDimension(
selectedLicense.getQtyDimension())
.build();
List<LicenseToken> result = client.getLicenseCheckoutClient()
.checkoutLicense(
claims,
Collections.singletonList(
checkoutArguments));
if (result.size() != 1) {
throw ExceptionBuilder.internalError(
"Expecting 1 license token from"
+ " checkout 1 license, got "
+ result.size()
+ " instead");
} else {
return result.get(0);
}
}
Finally, release the license based on the current checkout.
private LicenseReleaseResult releaseLicense(
final LicenseToken licenseToken) {
if (licenseToken.getLeaseId() == null) {
System.out.println(
"Null lease id, nothing to release.");
return null;
}
long qty = 1;
if (!QuantityDimension.SEATS.equals(
selectedLicense.getQtyDimension())) {
System.out.println(
"Enter final quantity that was consumed:");
qty = scanner.nextLong();
}
Map<String, String> claims = new HashMap<>();
LicenseReleaseArguments releaseArguments
= LicenseReleaseArguments
.builder()
.finalUsedQty(qty)
.leaseId(licenseToken.getLeaseId())
.build();
List<LicenseReleaseResult> result = client
.getLicenseCheckoutClient()
.releaseLicense(
claims,
Collections.singletonList(
releaseArguments));
if (result.size() != 1) {
throw ExceptionBuilder.internalError(
"Expecting 1 license release result"
+ " from release 1 license, got "
+ result.size()
+ " instead");
} else {
return result.get(0);
}
}