LOOK4 ID

LOOK4 ID provides

  • authorization for accessing web services, including LOOK4 Optics web services, based on the OAuth 2.0 standard,
  • single-sign-on authentication for the LOOK4 Optics portal and third-party web sites and applications, based on the OpenID Connect (OIDC) standard.

OAuth and OpenID Connect (OIDC)

Why?

OAuth 2.0 is a delegation protocol, allowing a software client to act on behalf of a user with limited access rights, without impersonating the user.

In contrast, providing a software client (or third-party web service) with the user’s password would give the software client or third-party service full control over the user account and all related resources. It is also a dangerous practice since clients need to store the user’s password; the password would be compromised as soon as a client software or service is compromised. This and many other issues with this password-sharing antipattern are addressed with the use of OAuth.

The OAuth and OpenID Connect standards are widely used, e.g. for “Sign in with Google” and many similar services. Therefore, extensive tooling and documentation is available, and many developers, especially web and mobile developers, are already familiar with it.

OAuth and OIDC terms and their meaning in the context of LOOK4 ID

  • LOOK4 ID acts as the authorization server and Identity Provider (IdP).
  • Resource owners are LOOK4 ID users. Access rights depend on the assigned company in LOOK4 Optics, e.g. eye care professionals (ECPs) might be allowed to download catalogs, place orders, or retrieve order status for previously submitted orders; whereas a user belonging to a supplier company might be allowed to publish catalog updates.
  • Protected resources are web services (APIs), e.g. the LOOK4 Optics API.
  • An OAuth client is a piece of software that accesses the protected resource on behalf of the resource owner. For example, it could be a practice management software, the portal of a home delivery service provider, a POS mobile app, etc.
  • A relying party is a web application that outsources its user authentication to LOOK4 ID. Such a website is also an OAuth client.
  • Scopes are used for fine-grained access control and define subsets of functionality of a protected resource. Scopes allow to limit what a client can do on behalf of the user. Examples of subsets of functionalities of the LOOK4 Optics API would be “read catalogs” or “place orders” (see also documentation of the full list of scopes used). Clients can request a set of scopes, and the resource owner can decide whether to allow this request (or a subset of it). This user interaction is called consent. The resource owner’s consent is valid forever (unless actively revoked); this property of OAuth is called Trust on first use (“TOFU”).
  • Authorization Token: A short-lived (1 hour) token that can be added as Authorization header to all LOOK4 Optics web service requests. This is a called a bearer token. Client applications can treat bearer tokens as arbitrary strings that just need to be remembered and passed on the protected resource.
  • Refresh Token: A long-lived token that can be stored by a client. Used to get a new authorization token from the authorization server without requesting the user to login to LOOK4 ID again, or even without the user being present. Refresh tokens in LOOK4 ID have a sliding expiration of 1 year, i.e. every time the refresh token is used, its expiration date is updated to one year from now. If a refresh token is not used for 1 year, the client would have to redirect the resource owner to LOOK4 ID for a new login. A client allowed to use refresh tokens is called Offline client.
  • Authorization Flow or Grant Type: A protocol or workflow for how a client can obtain tokens from the authorization server. Supported flows are covered below.
  • Federation: A scenario where LOOK4 ID trusts a third-party authorization server, e.g. an existing portal for eye care professionals created by a supplier or a retail chain. This can enable the third-party portal (and related applications) to make use of resources protected by LOOK4 ID (at least parts of it) without requiring their end-users to create a LOOK4 ID account. Please contact our support if you are interested in this scenario as this (using a custom grant and client assertions) is not covered by our public documentation and needs a custom implementation on both ends.

OAuth in depth

If you are the author of a client software, you do not necessarily need a deep understand of OAuth. If you still want to dig deeper, the following resources can be a good start:

OAuth and OIDC libraries

We recommend using an authentication library or framework implementing OAuth2 for communication with the authorization server. Although this is not strictly necessary, it will make it easier to follow best practices and achieve a higher level of security, while writing less code yourself.

See the OAuth 2.0 web site for a list of client libraries for many frameworks and programming languages

When using OIDC for authentication purposes, using a library implementing the full OIDC protocol (including discovery, token validation etc.) is inevitable.

See the OpenID website for a list of certified OpenID Connect relying party libraries:

Implementations known to work well with LOOK4 ID (please let us know of your experiences so that we can extend this list over time and keep it updated):

Registering client applications

Client applications need to be registered with LOOK4 and get a unique client ID assigned.

A self-service portal for managing client applications in LOOK4 ID is planned; until it becomes available, please contact support@look4.de for registering or updating clients. Thank you very much!

For all the client types, please include the following information in your registration request:

  • suggested client_id
  • display name (as shown to the user in the consent screen)
  • type of application (see below)

Further optional information:

  • description text (up to 200 characters)

Include multiple languages for display name and description if your software is available in multiple languages.

Additional information is required depending on the grant type or “authorization flow”.

Types of Client Applications and suitable “Authorization Flows”

Browser-based JavaScript Applications

Supported Flows

If you can use a library that supports it, please use the “Authorization code” flow (grant type authorization_code) without a client secret and with the PKCE OAuth extension (RFC 7636).

If this is not possible, you can use the “Implicit” flow (grant type implicit), but please note that it might be necessary in the future to deprecate this flow due to security considerations.

This type of client cannot (and doesn’t need to) be an “offline client” and cannot use refresh tokens.

Registration

For registering this kind of client application, in addition to the general information listed above (“Registering client applications”), please include the following information in your request:

  • Grant type (either authorization_code or implicit)
  • At least one Redirect URI: The redirect URI used in your authorization requests must match exactly one of these, including protocol, port number etc. Please remember to include URIs also for your development and testing environments if applicable.
  • CORS origin URIs, i.e. protocol, domain and port number (in case of non-default port numbers). Please remember to include URIs also for your development and testing environments if applicable.
  • Post-logout redirect URIs (optional): If your client wants to offer a combined sign-out from the application and LOOK4 ID, and an automatic redirection back from the LOOK4 ID logout page to the application, you can specify one or multiple allowed redirect URIs for that purpose. Please remember to include URIs also for your development and testing environments if applicable.

Native Desktop and Mobile Clients

Supported Flows

Please use the “Authorization code” flow (grant type authorization_code) without a client secret and with the PKCE OAuth extension (RFC 7636).

Registration

For registering this kind of client application, in addition to the general information listed above (“Registering client applications”), please include the following information in your request:

  • Offline access required? Whether long-lived refresh tokens will be provided or not. Please note that all sign-ins requesting offline access need user consent.
  • At least one Redirect URI: The redirect URI used in your authorization requests must match exactly one of these, including protocol, port number etc. Please remember to include URIs also for your development and testing environments if applicable.
    • URIs in the form of http://127.0.0.1:1234, where 1234 can be any valid port number, are accepted by default; therefore, for native clients, you can dynamically choose a free port for the back channel. If you can make use of this pattern, no extra Redirect URI is needed.

Choosing the browser for the “front channel” interactions

For the actual LOOK4 ID login and consent, it is strongly recommended to use the system browser instead of an embedded browser window (web view). Firstly, this offers a better user experience, as the user is more likely to be already signed into LOOK4 ID in the default browser, therefore doesn’t need to type the password again. Secondly, user login credentials should not be exposed to client applications, which could be possible with using an embedded browser, as the hosting application has full control over it.

Low-level Client Implementation

Just in case that you really have to implement this without making use of a OAuth library or framework, or you want to get an idea of what is going on behind the scenes, these are the steps performed to obtain the token for an application with “offline” access:

Do not forget URL encoding for all URL parameters!

To perform the initial authentication (authorization code flow with PKCE extension):

  1. Start a HTTP listener on a free local port, e.g. http://127.0.0.1:43210.
  2. Create a random 32 byte array, apply web-safe BASE64 encoding and store the resulting string in variable state.
  3. Create another random 32 byte array, apply web-safe BASE64 encoding, and store the resulting string in variable code_verifier.
  4. Calculate the SHA256 hash of the code_verifier string, and store it as a web-safe BASE64 string as code_challenge.
  5. Launch the system browser to the following LOOK4 ID URL:
GET /connect/authorize?
client_id=yourclientid&
scope=offline_access scope1 scope2 scope3& //(see list of scopes below)
response_type=code&
redirect_uri=http://127.0.0.1:xxxxx&
state=state-variable&
code_challenge=code_challenge-variable&
code_challenge_method=S256
  1. User logs into LOOK4 ID (if not already authenticated) and accepts the request of the application to get access as requested by the specified scopes.
  2. User gets redirected to the client application redirect URI, adding the following URL parameters to the provided redirect UI:
code=authorizationcode&
state=state-from-first-request
  1. Write a HTML document to your HTTP listener, telling the user to close the browser and to return to your application. Close the localhost HTTP listener.
  2. Make sure that the state URL parameter matches your state variable. If not, abort the authorization process.
  3. Send the following request to the LOOK4 ID token endpoint to “trade in” the one-time authorization code (parameters sent forms-encoded as POST request):
POST /connect/token
client_id=client1&
grant_type=authorization_code&
code=authorization-code-received-via-redirect
code_verifier=code_verifier-variable
redirect_uri=http://127.0.0.1:xxxxx
  1. In case of a valid request, the answer to this requests contains a small JSON document:
{
   "access_token": "…",
   "refresh_token": "…",
   "token_type": "Bearer"
}
  1. Persist the refresh token for later use in encrypted form. E.g. on Windows, use can use DPAPI (Data Protection API).

To get a new access token using the refresh token:

POST /connect/token
client_id=client1&
grant_type=refresh_token&
refresh_token=refresh-token

Server-Based Web Applications

Supported Flows

Please use the “Authorization code” flow (grant type authorization_code) with a client secret. We will provide you with a secret upon registration. Later on, you will be able to create and disable secrets via a self-service client management portal in LOOK4 ID.

Registration

For registering this kind of client application, in addition to the general information listed above (“Registering client applications”), please include the following information in your request:

  • Offline access required? Whether long-lived refresh tokens will be provided or not. Please note that all sign-ins requesting offline access need user consent.
  • At least one Redirect URI: The redirect URI used in your authorization requests must match exactly one of these, including protocol, port number etc. Please remember to include URIs also for your development and testing environments if applicable.

Absolute Redirect URL Required

Please note that your application, for providing the correct redirect URL, must be aware of its full external URL, including the correct protocol, domain name and port. This is a common source of problems for web applications running behind a load balancer or proxy.

Low-level Client Implementation

Just in case that you really have to implement this without making use of a OAuth library or framework, or you want to get an idea of what is going on behind the scenes, these are the steps performed to obtain the token:

Do not forget URL encoding for all URL parameters!

  1. On first use, the client redirects the user to the following LOOK4 ID URL:
GET /connect/authorize?
client_id=yourclientid&
scope=scope1 scope 2 scope3& //(see list of scopes below)
response_type=code&
redirect_uri=https://myapp/callback&
state=abc //(client-generated unique random value for CSRF protection)
  1. User logs into LOOK4 ID (if not already authenticated) and accepts the request of the application to get access as requested by the specified scopes.
  2. User gets redirected to the client application redirect URI, adding the following URL parameters to the provided redirect UI:
code=authorizationcode&
state=state-from-first-request //(clients should make sure that this matches the state in the original redirect to LOOK4 ID to protect their users from CSRF attacks)
  1. The client application directly contacts the LOOK4 ID token endpoint to “trade in” the one-time authorization code (parameters sent forms-encoded as POST request):
POST /connect/token
client_id=client1&
client_secret=secret&
grant_type=authorization_code&
code=authorization-code-received-via-redirect
redirect_uri=https://myapp/callback
  1. In case of a valid request, the answer to this requests contains a small JSON document:
{
   "access_token": "…",
   "refresh_token": "…",
   "token_type": "Bearer"
}

To get a new access token using the refresh token:

POST /connect/token
client_id=client1&
client_secret=secret&
grant_type=refresh_token&
refresh_token=refresh-token

Example of a Low-Level Implementation in ASP.NET Core

This is for demonstration only. Normally, in ASP.NET Core you would want to use the built-in OAuth support.

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Mime;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace L4IDWebAppWithoutOAuthLib.Controllers
{
   [Route("/")]
   public class HomeController : Controller
   {
      private const string ClientId = "...";
      private const string ClientSecret = "...";
      private const string Scopes = "offline_access l4o.supplierreg.readwrite";

      private static readonly Uri L4IDBaseUrl = new Uri("https://auth-test.look4optics.com");
      private static readonly Uri L4OAPIBaseUrl = new Uri("https://api-test.look4optics.com");
      private static readonly HttpClient L4IDClient = new HttpClient() { BaseAddress = L4IDBaseUrl };
      private static readonly HttpClient L4OAPIClient = new HttpClient() { BaseAddress = L4OAPIBaseUrl };

      // Redirect user to LOOK4 ID for sign-in
      [HttpGet("login")]
      public IActionResult Login()
      {
            // generate unique state to prevent e.g. CSRF
            string state = null;
            using (RandomNumberGenerator rng = new RNGCryptoServiceProvider())
            {
               byte[] randomBytes = new byte[16];
               rng.GetBytes(randomBytes);
               state = Convert.ToBase64String(randomBytes);
            }
            TempData["oauth-state"] = state; // could be persited in server-side session or cookie

            StringBuilder urlParamsBuilder = new StringBuilder();
            urlParamsBuilder.Append($"?client_id={Uri.EscapeDataString(ClientId)}");
            urlParamsBuilder.Append($"&scope={Uri.EscapeDataString(Scopes)}");
            urlParamsBuilder.Append($"&response_type=code");
            urlParamsBuilder.Append($"&redirect_uri={Uri.EscapeDataString(getCallbackUrl())}");
            urlParamsBuilder.Append($"&state={Uri.EscapeDataString(state)}");
            // nonce only required for OpenID Connect

            Uri authorizeUrl = new Uri(L4IDBaseUrl, $"/connect/authorize{urlParamsBuilder}");

            return Redirect(authorizeUrl.ToString());
      }

      [HttpGet("callback")]
      public async Task<IActionResult> Callback([FromQuery(Name = "code")] string code, [FromQuery(Name = "state")] string state)
      {
            // state validation
            if (string.IsNullOrEmpty(state))
            {
               return BadRequest("no state returned");
            }

            string expectedState = TempData["oauth-state"]?.ToString();
            if (string.IsNullOrEmpty(expectedState))
            {
               return BadRequest("no state available for this session");
            }

            if (state != expectedState)
            {
               return BadRequest("Invalid state");
            }

            if (string.IsNullOrEmpty(code))
            {
               return BadRequest("no code provided");
            }

            // use authorization code to get access and/or refresh token from L4ID token endpoint
            var tokenRequest = new FormUrlEncodedContent(new KeyValuePair<string, string>[]
               {
                  new KeyValuePair<string, string>("client_id", ClientId),
                  new KeyValuePair<string, string>("client_secret", ClientSecret),
                  new KeyValuePair<string, string>("grant_type", "authorization_code"),
                  new KeyValuePair<string, string>("code", code),
                  new KeyValuePair<string, string>("redirect_uri", getCallbackUrl())
               });
            var tokenResponse = await L4IDClient.PostAsync("/connect/token", tokenRequest);
            var tokenResponseJsonDoc = await JsonDocument.ParseAsync(await tokenResponse.Content.ReadAsStreamAsync());

            if (!tokenResponse.IsSuccessStatusCode)
            {
               return BadRequest(tokenResponseJsonDoc);
            }

            // TODO: if your requested and accepted scopes include "offline_access",
            // you will get a refresh token that can be persisted for this user e.g. in a database
            // and used to get a fresh access token any time later
            var refreshToken = tokenResponseJsonDoc.RootElement.GetProperty("refresh_token").GetString();

            // use can use this to access L4OAPI for up to 1h right away
            var accessToken = tokenResponseJsonDoc.RootElement.GetProperty("access_token").GetString();

            // example: get supplier registrations and return the raw JSON result
            var supplierRegistrationRequest = new HttpRequestMessage(HttpMethod.Get, "/v2/supplierregistrations");
            supplierRegistrationRequest.Headers.Add("Authorization", $"Bearer {accessToken}");
            supplierRegistrationRequest.Headers.Add("Accept", MediaTypeNames.Application.Json);
            var supplierRegistrationResponse = await L4OAPIClient.SendAsync(supplierRegistrationRequest);
            return Content(
               await supplierRegistrationResponse.Content.ReadAsStringAsync(),
               MediaTypeNames.Application.Json);
      }

      private string getCallbackUrl()
      {
            // absolute URL; this must be defined via configuration if your application is running behind a load balancer or reverse proxy!
            return Url.Action("callback", null, null, Request.Scheme);
      }
   }
}

Backend or Batch Applications

Applications that do not act on behalf of a LOOK4 ID user and do not need or support interactive login.

A typical use case in context of the LOOK4 Optics API would be backend or batch processes used by suppliers, or ECPs running their own custom software processes.

Supported Flows

Please use the “Client credentials” flow (grant type client_credentials) with a client secret. We will provide you with a secret upon registration. Later on, you will be able to create and disable secrets via a self-service client management portal in LOOK4 ID.

Limitations

All clients of this type must be confidential (i.e. server-based), and permissions (claims) granted to the client will be managed by LOOK4 internally.

This type of client cannot (and doesn’t need to) be an “offline client” and cannot use refresh tokens.

This type of client cannot use OIDC or any OpenID-related scopes (as there is no user identity).

Registration

For registering this kind of client application, in addition to the general information listed above (“Registering client applications”), please include the following information in your request:

  • Required access permissions for LOOK4 Optics API: scopes, implicit claims e.g. for supplier ID or company ID

Example using Bash, cURL, jq

# configure environment
token_endpoint="https://auth-test.look4optics.com/connect/token"
api_base_url="https://api-test.look4optics.com"

# L4IDv2 client config
client_id="..."
client_secret="..."
scopes="l4o.catalogs.read l4o.supplierreg.read"

# get access token
access_token=$(curl --silent \
--data client_id=$client_id \
--data client_secret=$client_secret \
--data grant_type="client_credentials" \
--data scope="$scopes" \
-X POST $token_endpoint \
| jq -r '.access_token')

# use access token to get current supplier registrations
curl -i \
-H "Authorization: Bearer $access_token" \
"$api_base_url/v2/supplierregistrations"

Example using .NET

This example is using the IdentityModel library.

Please refer to the documentation of your favorite authentication library on how to obtain a token using other libraries.

using IdentityModel.Client;
using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace Client
{
   public class Program
   {
      private static async Task Main()
      {
         using (var client = new HttpClient())
         {
            // discover endpoints from metadata
            var disco = await
            client.GetDiscoveryDocumentAsync("https://auth-test.look4optics.com").ConfigureAwait(false);

            if (disco.IsError)
            {
               Console.WriteLine(disco.Error);
               return;
            }

            // request token
            var tokenResponse = await client.RequestClientCredentialsTokenAsync(new
            ClientCredentialsTokenRequest
            {
               Address = disco.TokenEndpoint,
               ClientId = "quickstart-console",
               ClientSecret = "your-client-id",
               Scope = "l4o.catalogs.read"
            });

            if (tokenResponse.IsError)
            {
               Console.WriteLine(tokenResponse.Error);
               return;
            }

            // call api
            client.SetBearerToken(tokenResponse.AccessToken);

            var response = await
            client.GetAsync("https://api-test.look4optics.com/v2/catalogs/");

            Console.WriteLine(response.StatusCode);
         }
      }
   }
}

OpenID Connect (OIDC) support

If an application does not only want to make API calls on behalf of the user, but also needs to know the user’s identity, OIDC allows to authenticate the user.

This is only supported with the Autorization code and Implicit flows. Please remember to include the OIDC-related scopes you might need (at least openid, optionally profile, email), when registering your client and when requesting the token.

Instead of requesting the id_token response type, please use the access token to request profile data from the User Info Endpoint.

Requesting User Information

In this example, the openid and email scopes were included when requesting the token.

GET /connect/userinfo
Authorization: Bearer current-access-token

Example result:

{"sub":"12345","email":"user@example.net"}

sub will always be included and remain constant for that user, while the e-mail address might change. So if you want to identify a unique user across sessios, please use the sub claim for matching.

Token revocation

Revoking (short-lived) access tokens is not supported, as LOOK4 ID is using JWT (i.e. signed, self-contained) tokens.

Revoking (long-lived) refresh tokens can be useful if you want to enforce a user to reauthenticate with LOOK4 ID, or if a user is no longer using your or our service.

Note: If you are aware of a security incident involving LOOK4 ID tokens, e.g. tokens possible being exposed to unauthorized users or the public, in addition to revoking long-lived tokens, please always inform our support team so that we can search for abuse, analyze threats and take steps to protect our users and partners.

Using OAuth (according to RFC 7009)

You can either use your OAuth client library to do so, or, if you like to issue a raw HTTP call, use the following call to the revocation endpoint:

For confidential clients (with a client secret):

POST /connect/revocation
Content-Type: application/x-www-form-urlencoded
Authorization: Basic client-id-and-secret

token=token-to-be-revoked&token_type_hint=refresh_token

For public clients (without a client secret):

POST /connect/revocation
Content-Type: application/x-www-form-urlencoded

token=token-to-be-revoked&token_type_hint=refresh_token&client_id=clientid

Using LOOK4 ID Portal

Users can manage their own consents and revoke access to applications. When the user revokes access to an application, associated refresh tokens are revoked automatically.

Scopes

When requesting multiple scopes, separate scope names with spaces.

Special scopes (independent from the protected resource):

  • offline_access: requests a refresh token (if your client is registered as “offline client”).
  • In the OIDC authentication context (or for applications that use LOOK4 ID for Single Sign-On):
    • openid: requests the user’s identity (OIDC). Required to include if you want to use any of the other OIDC scopes or if you want to call the UserInfo endpoint.
    • profile: requests access to the user’s profile claims: family_name, given_name, locale, zoneinfo
    • email: requests access to the user’s email claim.
    • phone: requests access to the user’s phone_number claim.

Please refer to the LOOK4 Optics API documentation for a complete list of scopes supported.

Localization

The LOOK4 ID portal automatically decides the default language from the browser’s Accept-Language header. If you want to ensure that the LOOK4 ID language is consistent with the language of your client application, add the ui_locales request parameter to the query string when redirecting users to LOOK4 ID for authentication. You can include multiple values as a space-separated list, ordered by preference.

Example (without URL encoding):

?ui_locales=de en.

Supported UI languages

Currently, the LOOK4 ID user interface is available in English language. A German translation is in development. Further (european) languages will be added according to the needs of our partners.

Currently supported values for ui_locales:

  • en
Contact / Legal