• Skip to primary navigation
  • Skip to main content
  • Skip to footer

Cisco Umbrella

Enterprise network security

  • Contact Sales
  • Login
    • Umbrella Login
    • Cloudlock Login
  • Why Us
    • Why Cisco Umbrella
      • Why Try Umbrella
      • Why DNS Security
      • Why Umbrella SASE
      • Our Customers
      • Customer Stories
      • Why Cisco Secure
    • Fast Reliable Cloud
      • Global Cloud Architecture
      • Cloud Network Status
      • Global Cloud Network Activity
    • Unmatched Intelligence
      • A New Approach to Cybersecurity
      • Interactive Intelligence
      • Cyber Attack Prevention
      • Umbrella and Cisco Talos Threat Intelligence
    • Extensive Integrations
      • IT Security Integrations
      • Hardware Integrations
      • Meraki Integration
      • Cisco Umbrella and SecureX
  • Products
    • Cisco Umbrella Products
      • Cisco Umbrella Cloud Security Service
      • Recursive DNS Services
      • Cisco Umbrella SIG
      • Umbrella Investigate
      • What’s New
    • Product Packages
      • Cisco Umbrella Package Comparison
      • – DNS Security Essentials Package
      • – DNS Security Advantage Package
      • – SIG Essentials Package
      • – SIG Advantage Package
      • Umbrella Support Packages
    • Functionality
      • DNS-Layer Security
      • Secure Web Gateway
      • Cloud Access Security Broker (CASB)
      • Cloud Data Loss Prevention (DLP)
      • Cloud-Delivered Firewall
      • Cloud Malware Protection
      • Remote Browser Isolation (RBI)
    • Man on a laptop with headphones on. He is attending a Cisco Umbrella Live Demo
  • Solutions
    • SASE & SSE Solutions
      • Cisco Umbrella SASE
      • Secure Access Service Edge (SASE)
      • What is SASE
      • What is Security Service Edge (SSE)
    • Functionality Solutions
      • Web Content Filtering
      • Secure Direct Internet Access
      • Shadow IT Discovery & App Blocking
      • Fast Incident Response
      • Unified Threat Management
      • Protect Mobile Users
      • Securing Remote and Roaming Users
    • Network Solutions
      • Guest Wi-Fi Security
      • SD-WAN Security
      • Off-Network Endpoint Security
    • Industry Solutions
      • Government and Public Sector Cybersecurity
      • Financial Services Security
      • Cybersecurity for Manufacturing
      • Higher Education Security
      • K-12 Schools Security
      • Healthcare, Retail and Hospitality Security
      • Enterprise Cloud Security
      • Small Business Cybersecurity
  • Resources
    • Content Library
      • Top Resources
      • Cybersecurity Webinars
      • Events
      • Research Reports
      • Case Studies
      • Videos
      • Datasheets
      • eBooks
      • Solution Briefs
    • International Documents
      • Deutsch/German
      • Español/Spanish
      • Français/French
      • Italiano/Italian
      • 日本語/Japanese
    • For Customers
      • Support
      • Customer Success Webinars
      • Cisco Umbrella Studio
    • Get the 2022 Cloud Scurity Comparison Guide
  • Trends & Threats
    • Market Trends
      • Hybrid Workforce
      • Rise of Remote Workers
      • Secure Internet Gateway (SIG)
    • Security Threats
      • How to Stop Phishing Attacks
      • Malware Detection and Protection
      • Ransomware is on the Rise
      • Cryptomining Malware Protection
      • Cybersecurity Threat Landscape
      • Global Cyber Threat Intelligence
      • Cyber Threat Categories and Definitions
    •  
    • Woman connecting confidently to any device anywhere
  • Partners
    • Channel Partners
      • Partner Program
      • Become a Partner
    • Service Providers
      • Secure Connectivity
      • Managed Security for MSSPs
      • Managed IT for MSPs
    •  
    • Person looking down at laptop. They are connecting and working securely
  • Blog
    • News & Product Posts
      • Latest Posts
      • Products & Services
      • Customer Focus
      • Feature Spotlight
    • Cybersecurity Posts
      • Security
      • Threats
      • Cybersecurity Threat Spotlight
      • Research
    •  
    • Register for a webinar - with illustration of connecting securely to the cloud
  • Contact Us
  • Umbrella Login
  • Cloudlock Login
  • Free Trial
Security

Implementing OAuth for Registry V2

By Jack Zheng
Posted on February 23, 2016
Updated on April 16, 2020

Share

FacebookTweetLinkedIn

As the end of life for Docker registry V1 quickly approaching, the Quadra team has been working hard on the migration to Docker registry v2. We also saw this as a good opportunity to make some improvements to our current authentication setup for the registry, which uses Basic Authentication over HTTPS. After some experimentations, we’ve decided to try out the token based OAuth system, which will not only provide a much more sophisticated access control for our user images, but also allow us to use the same authentication across multiple registries. In this blog post, I will give an overview of OAuth workflow for Docker registries, and explain some of the implementation details which we’ve found to be poorly documented at the moment. Hopefully by the end of this, you will be able to roll out your own OAuth Server with Docker registry!

Just Show Me The Code

A sample implementation of our OAuth server can be found here. It’s a simple Flask app that understands the OAuth workflow and responds with a token that the v2 registry can understand. Instructions on how the setup the project can be found here, along with some explanations on its configurations.

OAuth Workflow

The OAuth authentication workflow for Docker registry can be described with the following steps:

  1. Client begins with a connection to the image registry
  2. If the image registry is properly setup with OAuth enabled, it will return a 401 error, and its response will contain information on how to authenticate
  3. The client then contacts the authorization server as instructed with the previous response from the registry
  4. The authorization server then returns a token representing the client’s access
  5. The client makes another request to the image registry, this time with the token embedded in its header
  6. The image registry tries to validate the token, and if successful, returns the resources requested by the client.

Let’s walk through a concrete example. First, start the demo project in our repo. Then let’s make a request to our local registry:

curl https://192.168.99.100:5000/v2/_catalog
*   Trying 192.168.99.100...
* Connected to 192.168.99.100 (192.168.99.100) port 5000 (#0)
* TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
* Server certificate: localhost
> GET /v2/_catalog HTTP/1.1
> Host: 192.168.99.100:5000
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
< Content-Type: application/json; charset=utf-8
< Docker-Distribution-Api-Version: registry/2.0
< Www-Authenticate: Bearer realm="http://192.168.99.100:8080/tokens",service="demo_registry",scope="registry:catalog:*"
< X-Content-Type-Options: nosniff
< Date: Sun, 31 Jan 2016 22:29:49 GMT
< Content-Length: 134
<
{"errors":[{"code":"UNAUTHORIZED","message":"authentication required","detail":[{"Type":"registry","Name":"catalog","Action":"*"}]}]}

As expected, the registry responded with a 401 error. More importantly however, the Www-Authenticate header in the response specifies how the client should authenticate. Let’s break it down:

  • Realm: realm tells the client where the OAuth server is located. In our case it points to http://192.168.99.100:8080/tokens
  • Service: service tells the OAuth server where the resources are hosted. In our case it’s our demo_registry
  • Scope: scope tells the OAuth server what kind of permissions are needed. In our case we are asking for admin access on the catalog endpoint.

Now we should have all the information we need to contact our OAuth server. To be consistent with the way the docker client authenticates, let’s also pass our user credentials in the header in the HTTP Basic Auth format. First, base64 encode our credentials:

$ echo -n username:password | base64
dXNlcm5hbWU6cGFzc3dvcmQ=

Then we can embed the result in our headers as follows:

curl -H "Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=" http://192.168.99.100:8080/tokens?service=demo_registry&scope=registry:catalog:*

*   Trying 192.168.99.100...
* Connected to 192.168.99.100 (192.168.99.100) port 8080 (#0)
> GET /tokens?service=demo_registry&scope=registry:catalog:* HTTP/1.1
> Host: 192.168.99.100:8080
> User-Agent: curl/7.43.0
> Accept: */*
> Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
>
< HTTP/1.1 200 OK
< Server: gunicorn/19.4.5
< Date: Sun, 31 Jan 2016 22:58:54 GMT
< Connection: close
< Content-Type: application/json
< Content-Length: 719
<
{
 "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IllJMkI6N01JUTpIT0I0OjdXN0I6Uk5NTDpaRUZVOkdLMkc6VkM3RTo3UUhHOkdVR1Y6T1FYVTozN0lUIn0.eyJzdWIiOiIiLCJpc3MiOiJkZW1vX29hdXRoX3NlcnZlciIsImFjY2VzcyI6W3sidHlwZSI6InJlZ2lzdHJ5IiwibmFtZSI6ImNhdGFsb2ciLCJhY3Rpb25zIjpbIioiXX1dLCJleHAiOjE0NTQyODQ3MzQsImlhdCI6MTQ1NDI4MTEzNCwibmJmIjoxNDU0MjgxMTM0LCJhdWQiOiJkZW1vX3JlZ2lzdHJ5In0.QYGsEkuFv5Mpg2_2oov3KylQcYZEhXJXGKB_ahDCmya4MUnyprRISFfk3Eovvc5OgGWUQx5-Gl7eSBidVI0z7K29wUV7ITL5prnbwg5pIjxJAYLkzBCmouiAyE24Uxy2vkVtDTicWsWT7H54Ou_v2umv7bQe6JB3t6vYsmb3taiDUI_RTWxfSOp7OK1n6UVFEEUHiV57wP3aWZ60A379a9ZP6sEHKhEi306OvXPyaz804KFH7sTqbSMYf9DP_Gy8Jh04Tw9zKmClk-byct8Hspelw1JytbsQonlKwV9OH30DTCjgaWyNiavTTdfpRmiDRRMRsROjw2JLL8ZMMTZEhQ"
}

As expected, the oauth server responded with our token. With this token, now we can make another request to the registry:

curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IllJMkI6N01JUTpIT0I0OjdXN0I6Uk5NTDpaRUZVOkdLMkc6VkM3RTo3UUhHOkdVR1Y6T1FYVTozN0lUIn0.eyJzdWIiOiIiLCJpc3MiOiJkZW1vX29hdXRoX3NlcnZlciIsImFjY2VzcyI6W3sidHlwZSI6InJlZ2lzdHJ5IiwibmFtZSI6ImNhdGFsb2ciLCJhY3Rpb25zIjpbIioiXX1dLCJleHAiOjE0NTQyODQ3MzQsImlhdCI6MTQ1NDI4MTEzNCwibmJmIjoxNDU0MjgxMTM0LCJhdWQiOiJkZW1vX3JlZ2lzdHJ5In0.QYGsEkuFv5Mpg2_2oov3KylQcYZEhXJXGKB_ahDCmya4MUnyprRISFfk3Eovvc5OgGWUQx5-Gl7eSBidVI0z7K29wUV7ITL5prnbwg5pIjxJAYLkzBCmouiAyE24Uxy2vkVtDTicWsWT7H54Ou_v2umv7bQe6JB3t6vYsmb3taiDUI_RTWxfSOp7OK1n6UVFEEUHiV57wP3aWZ60A379a9ZP6sEHKhEi306OvXPyaz804KFH7sTqbSMYf9DP_Gy8Jh04Tw9zKmClk-byct8Hspelw1JytbsQonlKwV9OH30DTCjgaWyNiavTTdfpRmiDRRMRsROjw2JLL8ZMMTZEhQ" https://192.168.99.100:5000/v2/_catalog

*   Trying 192.168.99.100...
* Connected to 192.168.99.100 (192.168.99.100) port 5000 (#0)
* TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
* Server certificate: localhost
> GET /v2/_catalog HTTP/1.1
> Host: 192.168.99.100:5000
> User-Agent: curl/7.43.0
> Accept: */*
> Authorization: Bearer
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IllJMkI6N01JUTpIT0I0OjdXN0I6Uk5NTDpaRUZVOkdLMkc6VkM3RTo3UUhHOkdVR1Y6T1FYVTozN0lUIn0.eyJzdWIiOiIiLCJpc3MiOiJkZW1vX29hdXRoX3NlcnZlciIsImFjY2VzcyI6W3sidHlwZSI6InJlZ2lzdHJ5IiwibmFtZSI6ImNhdGFsb2ciLCJhY3Rpb25zIjpbIioiXX1dLCJleHAiOjE0NTQyODQ3MzQsImlhdCI6MTQ1NDI4MTEzNCwibmJmIjoxNDU0MjgxMTM0LCJhdWQiOiJkZW1vX3JlZ2lzdHJ5In0.QYGsEkuFv5Mpg2_2oov3KylQcYZEhXJXGKB_ahDCmya4MUnyprRISFfk3Eovvc5OgGWUQx5-Gl7eSBidVI0z7K29wUV7ITL5prnbwg5pIjxJAYLkzBCmouiAyE24Uxy2vkVtDTicWsWT7H54Ou_v2umv7bQe6JB3t6vYsmb3taiDUI_RTWxfSOp7OK1n6UVFEEUHiV57wP3aWZ60A379a9ZP6sEHKhEi306OvXPyaz804KFH7sTqbSMYf9DP_Gy8Jh04Tw9zKmClk-byct8Hspelw1JytbsQonlKwV9OH30DTCjgaWyNiavTTdfpRmiDRRMRsROjw2JLL8ZMMTZEhQ
>
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< Docker-Distribution-Api-Version: registry/2.0
< X-Content-Type-Options: nosniff
< Date: Sun, 31 Jan 2016 23:58:06 GMT
< Content-Length: 20
<
{"repositories":[]}

Finally, the registry returns our desired response after it validated our token.

The OAuth Token

Now that we are familiar with the OAuth workflow, we should also learn about the OAuth Token and how the OAuth server should generate them.

The Docker Registry accepts a well-known token format called JSON Web Token or JWT as its authentication token. The JWT token consists of three parts separated by periods (.): Header, Claim, and Signature. A typical JWT token will look like this:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IllJMkI6N01JUTpIT0I0OjdXN0I6Uk5NTDpaRUZVOkdLMkc6VkM3RTo3UUhHOkdVR1Y6T1FYVTozN0lUIn0.eyJzdWIiOiIiLCJpc3MiOiJkZW1vX29hdXRoX3NlcnZlciIsImFjY2VzcyI6W3sidHlwZSI6InJlZ2lzdHJ5IiwibmFtZSI6ImNhdGFsb2ciLCJhY3Rpb25zIjpbIioiXX1dLCJleHAiOjE0NTQyODQ3MzQsImlhdCI6MTQ1NDI4MTEzNCwibmJmIjoxNDU0MjgxMTM0LCJhdWQiOiJkZW1vX3JlZ2lzdHJ5In0.QYGsEkuFv5Mpg2_2oov3KylQcYZEhXJXGKB_ahDCmya4MUnyprRISFfk3Eovvc5OgGWUQx5-Gl7eSBidVI0z7K29wUV7ITL5prnbwg5pIjxJAYLkzBCmouiAyE24Uxy2vkVtDTicWsWT7H54Ou_v2umv7bQe6JB3t6vYsmb3taiDUI_RTWxfSOp7OK1n6UVFEEUHiV57wP3aWZ60A379a9ZP6sEHKhEi306OvXPyaz804KFH7sTqbSMYf9DP_Gy8Jh04Tw9zKmClk-byct8Hspelw1JytbsQonlKwV9OH30DTCjgaWyNiavTTdfpRmiDRRMRsROjw2JLL8ZMMTZEhQ

Let’s find out how each part of the JWT token is generated.

Header

A JWT Header typically look like this before encoding (formatted with white space for readability):

{
            "typ": "JWT",
            "alg": "RS256",
            "kid": "ABCD:EFGH:IJKL:MNOP:QRST:UVWX:YZ23:4567:ABCD:EFGH:IJKL:MNOP"
 }

The “typ” field specifies the type of the token, and is usually set to “JWT.” The “alg” field specifies the algorithm used to generate the signature part of the token. For simplicity, our OAuth server only supports the RS256 Algorithm. A list of commonly used values for this field is listed below:

Screen Shot 2016-02-23 at 12.56.46 PM


Last but not the least, the “kid” field is an unique ID generated from the public part of the signing key. It was the source of a fair bit of frustration for us because there are no documentations on how this field should be generated! It wasn’t until we dug through the Github repository for Docker registry source code that we finally solved it.

So, long story short, for Docker registry, the “kid” field should be implemented as follows:

Take the SHA256 hash of the DER encoded public key, and truncate it to 240 bits. The result of that is then encoded into 12 base32 groups. The final result look like this:

ABCD:EFGH:IJKL:MNOP:QRST:UVWX:YZ23:4567:ABCD:EFGH:IJKL:MNOP

You can find our implementation of the algorithm here.

Now that we have all the fields we need, we can create the actual header: strip all white spaces from the header, the result of which is base64 urlsafe encoded to form the first part of the token.

Claim

Also known as the JWT payload, the claim set for the JWT token typically look like this:

{
 "sub": "",
 "iss": "demo_oauth_server",
 "access": [
   {
     "type": "registry",
     "name": "catalog",
     "actions": [
       "*"
     ]
   }
 ],
 "exp": 1454284734,
 "iat": 1454281134,
 "nbf": 1454281134,
 "aud": "demo_registry"
}

The “iss” field specifies where the token is issued from, usually the FQDN of the OAuth Server is used. The “aud” field specifies the intended audience of the token, usually this is set to the FQDN of the docker registry. The ‘exp’ field specifies the expiration time of the token in unix time. The ‘nbf’ (not before) field specifies the earliest time when the token is valid. The “iat” field specifies when the token is issued.

The “access” field specifies what kind of permission this token has. In our example, the token granted us “push and pull” permissions on the repository named samalba/my-app. The Oauth server should generate this field based on the access request generated during the first connection to docker registry.

Again, white spaces are stripped from the claim and the result is then base64 urlsafe encoded to form the second part of the token.

Signature

The signature is generated by taking the base64 urlsafe encoded header concatenated with the base64 urlsafe encoded claim using a period and signing it with the key and algorithm specified in the header. Using the header and claim given above, the pseudo code to generate the signature would be the following:

RS256(base64_urlsafe_encode(header) + “.” + 
base64_urlsafe_encode(claim),  key)

The result is then concatenated to the token as the final part.

A live debugger can be found at https://jwt.io/, and it can be a very useful way to make sure your tokens are valid.

Conclusion

Rolling out our own OAuth server wasn’t easy, and we hope that this blog has provided you with a good starting point so you don’t have to repeat some of the problems we faced. Feel free to play around with the OAuth server app and submit changes or improvements!

Previous Post:

Previous Article

Next Post:

Next Article

Follow Us

  • Twitter
  • Facebook
  • LinkedIn
  • YouTube

Footer Sections

What we make

  • Cloud Security Service
  • DNS-Layer Network Security
  • Secure Web Gateway
  • Security Packages

Who we are

  • Global Cloud Architecture
  • Cloud Network Status
  • Cloud Network Activity
  • OpenDNS is now Umbrella
  • Cisco Umbrella Blog

Learn more

  • Webinars
  • Careers
  • Support
  • Cisco Umbrella Live Demo
  • Contact Sales
Umbrella by Cisco
208.67.222.222+208.67.220.220
2620:119:35::35+2620:119:53::53
Sign up for a Free Trial
  • Cisco Online Privacy Statement
  • Terms of Service
  • Sitemap

© 2023 Cisco Umbrella