How do I troubleshoot 401 errors on Looker API calls? (Troubleshooting API authentication)

In short, a 401 error on an API call means that you did not properly authenticate to make the API call.

Nevertheless, these errors can be tricky to troubleshoot. This article walks through some common causes of this error.

Review: API Authentication

It's a good idea to review what successful authentication to the Looker API looks like before troubleshooting problems with authentication. The simplest way to test it out is by using a curl command, like in in the article What is a cURL API call and what does it look like. As a quick test, create API credentials for a Looker user and try the following commands (replacing the XXXXXXXXXX values with your API credentials):

$ curl -d "client_id=XXXXXXXXXX&client_secret=XXXXXXXXXX" https://EXAMPLE.cloud.looker.com/api/4.0/login

You should get back an access token:

{"access_token":"4QDkCyCtZzYgj4C2p2cj3csJH7zqS5RzKs2kTnG4","token_type":"Bearer","expires_in":3600,"refresh_token":null}

Then try calling the me API endpoint, passing your new access token into the Authorization header:

$ curl -H "Authorization: token 4QDkCyCtZzYgj4C2p2cj3csJH7zqS5RzKs2kTnG4" https://EXAMPLE.cloud.looker.com/api/4.0/user

You should get back a response that includes your Looker user info, indicating that the call was successful.

Regardless of what method you are using to authenticate (Postman or other software, supported API SDK, custom script, etc.), the authentication will follow this same basic process: hit the login endpoint, get back an API token, and use the token in subsequent calls.

Why do 401 errors happen?

A 401 error occurs when you do not properly follow the above steps to authenticate to the API. We can further break this down into a few different reasons:

1. You did not hit the login endpoint or go through the API authentication steps at all.
2. You hit the login endpoint but did not get back a token.
3. You passed an invalid or expired token to the API call.
4. You did not pass the token in the correct location (the Authorization header).
5. You made the API call on the web server port instead of the API server port (only applicable to Looker-hosted instances)

Let's go through each of these in detail:

1. You did not hit the login endpoint or go through the API authentication steps at all.

An example of this would be if you just ran the following curl command:

$ curl https://EXAMPLE.cloud.looker.com/api/4.0/user
{"message":"Requires authentication.","documentation_url":"https://cloud.google.com/looker/docs/"}

The command returns the "Requires authentication" 401 error because you did not supply the token to authenticate the call. Another example would be if you directly entered the URL for the endpoint into the browser bar.

Note that in the browser example, it is not even possible to modify the Authorization header to supply the token for the API call (at least without some fancy browser extensions), so this is not a valid way to make an API call in the first place.

2. You hit the login endpoint but did not get back a token.

If you supply invalid API credentials to the login endpoint, it will throw a 404 error (which will show up as the "Looker is unavailable" page on Looker-hosted instances).

 

$ curl -d "client_id=FAKEID&client_secret=FAKESECRET" https://EXAMPLE.cloud.looker.com/api/4.0/login
<!DOCTYPE html>
<html>
<head>
<title>Looker Not Found (404)</title>
<link href='https://fonts.googleapis.com/css?family=Open+Sans:400,600' rel='stylesheet' type='text/css'>

<!-- @@@@@@@@@@@@@ FAVICONS @@@@@@@@@@@@@ -->

<link rel="apple-touch-icon-precomposed" sizes="57x57" href="https://wwwstatic-b.lookercdn.com/favicon/apple-touch-icon-57x57.png" />
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="https://wwwstatic-c.lookercdn.com/favicon/apple-touch-icon-114x114.png" />
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="https://wwwstatic-d.lookercdn.com/favicon/apple-touch-icon-72x72.png" />
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="https://wwwstatic-a.lookercdn.com/favicon/apple-touch-icon-144x144.png" />
<link rel="apple-touch-icon-precomposed" sizes="60x60" href="https://wwwstatic-b.lookercdn.com/favicon/apple-touch-icon-60x60.png" />
<link rel="apple-touch-icon-precomposed" sizes="120x120" href="https://wwwstatic-c.lookercdn.com/favicon/apple-touch-icon-120x120.png" />
<link rel="apple-touch-icon-precomposed" sizes="76x76" href="https://wwwstatic-d.lookercdn.com/favicon/apple-touch-icon-76x76.png" />
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="https://wwwstatic-a.lookercdn.com/favicon/apple-touch-icon-152x152.png" />
<link rel="icon" type="image/png" href="https://wwwstatic-b.lookercdn.com/favicon/favicon-196x196.png" sizes="196x196" />
<link rel="icon" type="image/png" href="https://wwwstatic-c.lookercdn.com/favicon/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/png" href="https://wwwstatic-d.lookercdn.com/favicon/favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="https://wwwstatic-a.lookercdn.com/favicon/favicon-16x16.png" sizes="16x16" />
<link rel="icon" type="image/png" href="https://wwwstatic-b.lookercdn.com/favicon/favicon-128.png" sizes="128x128" />
<meta name="application-name" content="Looker"/>
<meta name="msapplication-TileColor" content="#FFFFFF" />
<meta name="msapplication-TileImage" content="https://wwwstatic-c.lookercdn.com/favicon/mstile-144x144.png" />
<meta name="msapplication-square70x70logo" content="https://wwwstatic-d.lookercdn.com/favicon/mstile-70x70.png" />
<meta name="msapplication-square150x150logo" content="https://wwwstatic-a.lookercdn.com/favicon/mstile-150x150.png" />
<meta name="msapplication-wide310x150logo" content="https://wwwstatic-b.lookercdn.com/favicon/mstile-310x150.png" />
<meta name="msapplication-square310x310logo" content="https://wwwstatic-c.lookercdn.com/favicon/mstile-310x310.png" />

<style type="text/css">
body {
background-color: #2e343f;
color: white;
height: auto;
font-family: Open Sans, Helvetica, Arial, sans-serif;
}
.message {
width: 100%;
max-width: 760px;
margin: 0 auto;
margin-top: 135px;
text-align: center;
}
h2, h3 {
font-weight: normal;
}
a {
color: white;
}
</style>
</head>
<body>

<div class="message">

<img width="210" height="84" src="https://wwwstatic-a.lookercdn.com/logos/looker_all_white.svg" alt="Looker">

<h1>Looker is unavailable.</h1>

<h2>If you typed in a URL, double-check the spelling.</h2>
<h2>This may also be due to a temporary condition such as an outage, <a href="https://docs.looker.com/relnotes/hosted-maintenance-hours">scheduled maintenance</a> or upgrade.</h2>
<br>
<h3>
If this message persists or you have any concerns, <br> contact us from
<a href="https://help.looker.com">help.looker.com</a> and we'll respond promptly.
</h3>

</div>

</body>
</html>

 

Obviously there is no token in the response, so most API scripts will stop here and not attempt to run the subsequent API call (our supported API SDKs should throw an error when you initialize the SDK object). However, if you created your own custom API script, and if you designed it to attempt to parse a token from the login response regardless of whether it hit a 404, this would result in passing a blank value for the authorization header in the subsequent call. The curl equivalent would look like this:

$ curl -H "Authorization: token " https://EXAMPLE.cloud.looker.com/api/4.0/user
{"message":"Requires authentication.","documentation_url":"https://cloud.google.com/looker/docs/"}

You should verify what's in raw response from the login endpoint. If the response contains a 404, then check to make sure that the API credentials exist for a user on your Looker instance.

3. You passed an invalid or expired token to the API call.

Here's an example of passing an invalid token:

$ curl -d "client_id=XXXXXXXXXX&client_secret=XXXXXXXXXX" https://EXAMPLE1.cloud.looker.com/api/4.0/login
{"access_token":"4QDkCyCtZzYgj4C2p2cj3csJH7zqS5RzKs2kTnG4","token_type":"Bearer","expires_in":3600,"refresh_token":null}


$ curl -H "Authorization: token 4QDkCyCtZzYgj4C2p2cj3csJH7zqS5RzKs2kTnG4" https://EXAMPLE2.cloud.looker.com/api/4.0/user
{"message":"Requires authentication.","documentation_url":"https://cloud.google.com/looker/docs/"}

In this example, the user hit the login endpoint on the EXAMPLE1 instance with correct credentials and it returned an access token, as expected. However, the user then supplied the token to an API call on a different Looker instance, EXAMPLE2. Since the token is not valid for that Looker instance, it throws a 401 error.

Additionally, as noted in our API auth doc, expired tokens or tokens for sessions that have already logged out will also throw 401 errors:

The OAuth 2.0 access token can be used on multiple API requests, until the access token expires or is invalidated by calling the logout endpoint. API requests that use an expired access token will fail with a 401 Authorization Required HTTP response.

You may have noticed the `"expires_in":3600` part of the login response in some of the examples above. All Looker API tokens expire after 3600 seconds (1 hour); this value is not configurable. So, API calls made with a token that is over 1 hour old will hit a 401 error.

4. You did not pass the token in the correct location (the Authorization header)

The token must be passed in the authorization header exactly like this:

`"Authorization: Bearer 4QDkCyCtZzYgj4C2p2cj3csJH7zqS5RzKs2kTnG4"`

or like this:

`"Authorization: token 4QDkCyCtZzYgj4C2p2cj3csJH7zqS5RzKs2kTnG4"`

Any other format will throw a 401 error. As an exercise, can you spot the issues in the following API calls?

$ curl -H "Authorization token 4QDkCyCtZzYgj4C2p2cj3csJH7zqS5RzKs2kTnG4" https://EXAMPLE.cloud.looker.com/api/4.0/user
{"message":"Requires authentication.","documentation_url":"https://cloud.google.com/looker/docs/"}

This throws a 401 error because there is no colon between "Authorization" and "token."

$ curl -H "Authorization: Token 4QDkCyCtZzYgj4C2p2cj3csJH7zqS5RzKs2kTnG4" https://EXAMPLE.cloud.looker.com/api/4.0/user
{"message":"Requires authentication.","documentation_url":"https://cloud.google.com/looker/docs/"}

This throws a 401 error because "Token" is capitalized. The syntax is case-sensitive.

$ curl -H "Content-Type: application/json" -d "{\"headers\": {\"Authorization\": \"token 4QDkCyCtZzYgj4C2p2cj3csJH7zqS5RzKs2kTnG4\"}}" https://EXAMPLE.cloud.looker.com/api/4.0/user
{"message":"Requires authentication.","documentation_url":"https://cloud.google.com/looker/docs/"}

This throws a 401 error because the user is attempting to pass the authorization header in the request body of the API call (the `-d` curl option) instead of passing it as an actual header with the `-H` curl option. It is pretty obvious that this format is incorrect in the curl context, but it may be less obvious if you are using another tool such as Postman or a custom script, where it may be unclear exactly how the tool is sending the values in the request. It is out of scope for Looker Support to help troubleshoot custom scripts, so you will need to be familiar with the request library you are using in their custom script to understand how the headers should be passed. (For example, in the Axios request.post method, you need to send the request body as the second argument and the request headers as the third argument. If you sent the headers as the second argument, you would get a 401 error.)

Looker's supported SDKs completely eliminate this pitfall by constructing the requests for you, so that you do not have to worry about how the token is being passed.

5. You made the API call on the web server port instead of the API server port (only applicable to customer hosted instances)

A quick recap on Looker API network architecture: Looker serves web application requests (accessing the Looker UI in the browser) on port 9999 by default, while it serves API requests on port 19999 by default. The default port for https requests is 443, so a typical setup is to set a proxy or load balancer in front of the Looker server that routes port 443 requests to either port 9999 or port 19999 on the Looker server.

On a Looker-hosted instance, our nginx proxy uses path-based routing, so that all requests with a path starting with `/api/4.0` will be automatically routed to the API server port 19999. This means that for a Looker-hosted instance, it's impossible to accidentally send an API request to the web server port 9999—the nginx proxy will ensure that API requests are routed correctly.

On a customer-hosted instance, however, customers do not always set up this path-based routing. A more typical customer-hosted setup is to simply route ALL requests on port 443 to the web server port 9999 for a cleaner URL in the browser. Our sample nginx configuration doc  shows an example of this setup. This means that to route a request to port 19999 on the Looker server, you must set port 19999 explicitly in the URL, like this:


$ curl -H "Authorization: token 4QDkCyCtZzYgj4C2p2cj3csJH7zqS5RzKs2kTnG4" https://EXAMPLE.customer.com:19999/api/4.0/user

If you neglect to specify the port for an API call with this port-forwarding setup, however, then the request will use the https default port 443, which will be routed to the Looker web server port 9999, which will not be able to serve the API request.

$ curl -H "Authorization: token 4QDkCyCtZzYgj4C2p2cj3csJH7zqS5RzKs2kTnG4" https://EXAMPLE.customer.com/api/4.0/user
{"error":"Unauthorized"}

We can distinguish this from the other causes of 401 errors based on the `{"error":"Unauthorized"}` message in the output. The output for all of the other causes of 401 errors discussed above was `{"message":"Requires authentication.","documentation_url":"https://cloud.google.com/looker/docs/"}`.

Note that all 401 errors show the status as `HTTP/1.1 401 Unauthorized` if you get the verbose output:

$ curl -v https://EXAMPLE.cloud.looker.com/api/4.0/user
* Trying 104.198.6.242:443...
* Connected to EXAMPLE.cloud.looker.com (104.198.6.242) port 443
* ALPN: curl offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
* CAfile: /etc/ssl/cert.pem
* CApath: none
* (304) (IN), TLS handshake, Server hello (2):
* (304) (IN), TLS handshake, Unknown (8):
* (304) (IN), TLS handshake, Certificate (11):
* (304) (IN), TLS handshake, CERT verify (15):
* (304) (IN), TLS handshake, Finished (20):
* (304) (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / AEAD-AES256-GCM-SHA384
* ALPN: server accepted http/1.1
* Server certificate:
* subject: CN=*.cloud.looker.com
* start date: Apr 19 07:05:16 2024 GMT
* expire date: Jul 18 07:05:15 2024 GMT
* subjectAltName: host "EXAMPLE.cloud.looker.com" matched cert's "*.cloud.looker.com"
* issuer: C=US; O=Google Trust Services LLC; CN=GTS CA 1P5
* SSL certificate verify ok.
* using HTTP/1.1
> GET /api/4.0/user HTTP/1.1
> Host: EXAMPLE.cloud.looker.com
> User-Agent: curl/8.4.0
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
< Date: Mon, 13 May 2024 21:10:08 GMT
< Content-Type: application/json
< Content-Length: 98
< Connection: keep-alive
< Vary: Accept-Encoding, Origin
< X-B3-TraceId: a4259f8bf035e00e2fca787a6365c5ad
< X-B3-SpanId: 86a1a104c39db98a
< X-B3-Sampled: 0
< Server-Timing: server_latency;dur=9.99
< X-Content-Type-Options: nosniff
< Strict-Transport-Security: max-age=31536000; includeSubDomains
<
* Connection #0 to host EXAMPLE.cloud.looker.com left intact
{"message":"Requires authentication.","documentation_url":"https://cloud.google.com/looker/docs/"}

However, an API call on the web server port instead of the API server port is the only one of the 5 causes where you will see the message `{"error":"Unauthorized"}` in the actual response.

Conclusion

As you can see, a 401 error really only has one root cause: you did not properly authenticate to make the call. However, it is easier than you might think to mess up the basic authentication steps—a mistake at any step along the way could result in a 401 error. Therefore, you should be prepared to go over each of these possibilities to find any flaws in your authentication process.

 

Contributors
Version history
Last update:
‎05-14-2024 11:07 AM
Updated by: