10Duke Scale SDK for Java
Loading...
Searching...
No Matches
Using Identity-Based Licensing

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), which is an authentication framework built on OAuth 2. 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 "Device Authorization Grant" authentication. Device authentication is mostly intended for scenarios, where the device does not have browser: The user authenticates out-of-band using 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, which is used to detect when the login is complete: When the login is complete, the OIDC server will issue HTTP-redirect to a pre-configured URL (the redirect-uri) in the browser. For details see RFC 8252: 7.3 Loopback Interface Redirection.

Configuration

The client has to be configured with HTTP-message, which is served by the HTTP-server after the login is complete. This HTTP message is a HTTP-response body (response message entity). The client application is also responsible for configuring the OIDC-provider's JSON Web Key Set URL and the OpenID Connect discovery URL where the OIDC-provider's authorization settings are advertised.

Device authentication

If the device, where the client is running, does not have a browser, or using the browser is awkward, the authentication can be done using "Device Authorization Grant", aka. device flow.

Workflow description

To start the user login, the device shows an authentication URL and so called "user code" to the user. Using another device (e.g. 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 user session is set up.

For details of the Device Authorization Grant, see RFC 8628.

When authenticating with device flow the application developer has to implement the display of the information relevant for the end user.

Configuration

The client application is responsible for configuring the OIDC-provider's JSON Web Key Set URL and the OpenID Connect discovery URL where the OIDC-provider's authorization settings are advertised.

Examples

The process of implementing identity-based license consumption is explained in 10Duke Scale documentation.

Desktop application example

First, lets start with looking at the main program of this example. The main program is a Java cli application, which implements the following flow:

  • initialize the application and the 10Duke Scale SDK
  • sign in the user, using OAuth PKCE flow
  • query information to enable selecting a license to checkout
  • checkout a seat
  • release the checkout
public static void main(final String[] args)
throws IOException, InterruptedException, URISyntaxException {
// Assert that a desktop (browser) is supported
if (!Desktop.isDesktopSupported()) {
throw new UnsupportedOperationException(
"Java AWT Desktop is not supported,"
+ " cannot run user sign-in");
}
CliLicensingExample example = new CliLicensingExample();
// Bootstrap the client app and sdk
example.initClientApp();
// Sign in the user and wait for completion
example.signin();
System.out.println("waiting for login to complete...");
example.loginSemaphore.acquire();
// Select licensee, one of potentially many
example.selectLicensee();
if (example.selectedLicensee == null) {
System.out.println("No licensee selected, bye bye...");
return;
}
// Select a license for checkout
example.selectLicense();
if (example.selectedLicense == null) {
System.out.println("No license selected, bye bye...");
return;
}
// Checkout the license
LicenseToken licenseToken = example.checkoutLicense();
System.out.println(
"License checkout made for product"
+ licenseToken.getProductName()
+ " with status: "
+ licenseToken.getStatus());
// Release the license
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()));
}
// Example end
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 a client application builder utility called DesktopAppClientBuilder.

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;
}
}

Sign in the user, this example shows using the OAuth PKCE flow. Running the PKCE flow will automatically update the SDK's authorization provider used for setting the HTTP Authorization header for upcoming license checkout calls. See more about application configuration in: setting up an application to use the 10Duke Scale SDK for Java. The call to signin() will trigger an asynchronous workflow where the user interacts with a browser to authenticate. Once the authentication with the identity provider completes control returns to the authCodeCallback(final String code, final String error) method. From there on the authorization code is exchanged for an access token. The access token response's ID-token is used to print a welcome message. The ID-token will also be used as means to authorize calls to the 10Duke Scale licensing API.

private void signin() throws IOException {
// getPkceClient() return a 10Duke client instance
// that has been configured for the client application.
// The following call will trigger user authentication
// using the desktop browser.
lastPkceInfo = client.getPkceClient().authorize(
(url) -> openUrl(url),
(code, err) -> authCodeCallback(code, err),
false, // wait results = false means async call
30); // max wait time
}

Query which Licensee's the user is associated with

private void selectLicensee() {
// Describe the different customer associations
// the user has (licensee relationships).
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);
}

Querying information about available licenses, this is based on having selected a Licensee in the previous step

private void selectLicense() {
// Describe licenses that the user has access to
// for a selected licensee
availableLicenses = client
.getLicenseCheckoutClient()
.describeLicenseConsumerLicenses(
selectedLicensee.getId(),
// license consumer id == null as the
// API will use our ID-token to determine
// the license consumer
null,
// filter field: no filtering applied
null,
// filter value: no filtering applied
null,
// with metadata == false, just intrested
// in main license information
false,
// offset == 0, means start at
// the beginning
0,
// limit to 25 first licenses
25,
// order ascending by product name
"productName",
true);
selectedLicense = selectLicense(availableLicenses.getLicenses());
}

Checkout 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 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);
}
}