Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Loop 302 after expire access token #84

Open
dolgovas opened this issue Nov 29, 2023 · 5 comments
Open

Loop 302 after expire access token #84

dolgovas opened this issue Nov 29, 2023 · 5 comments

Comments

@dolgovas
Copy link

Good day!
I ran my test installation through this guide (https://docs.nginx.com/nginx/deployment-guides/single-sign-on/keycloak/)

And started to get infinite loop after access token expired.
It seems strange.

First step go the site https://main.example.com/
step 2 -> 302 redirect to https://keycloak.example.com/
step 3 -> auth in keycloak
step 4 -> 302 to https://main.example.com/
step 5 -> after 5 minutes (access token ttl) browser started return 302 from main.example.com to keycloak, keycloak send 302 to main.example and infinite loop....

server {
  listen 443 ssl;
  server_name main.example.com;
  include conf.d/locations-openid;

  location / {
    include conf.d/locations-openid_auth_jwt;
    proxy_pass https://upstream;
  }

}



# conf.d/locations-openid_auth_jwt;
auth_jwt "" token=$session_jwt;
error_page 401 = @do_oidc_flow;
auth_jwt_key_request /_jwks_uri; # Enable when using URL
proxy_set_header username $jwt_claim_sub;


#conf.d/locations-openid
# Advanced configuration START
set $internal_error_message "NGINX / OpenID Connect login failure\n";
set $pkce_id "";
subrequest_output_buffer_size 32k; # To fit a complete tokenset response
gunzip on; # Decompress IdP responses if necessary
# Advanced configuration END
location = /_jwks_uri {
    auth_jwt off;
    internal;
    proxy_cache jwk;                              # Cache the JWK Set recieved from IdP
    proxy_cache_valid 200 12h;                    # How long to consider keys "fresh"
    proxy_cache_use_stale error timeout updating; # Use old JWK Set if cannot reach IdP
    proxy_ssl_server_name on;                     # For SNI to the IdP
    proxy_method GET;                             # In case client request was non-GET
    proxy_set_header Content-Length "";           # ''
    proxy_set_header Accept-Encoding "gzip";      # fixed OIDC authorization code sent but token response is not JSON
    proxy_pass $oidc_jwt_keyfile;                 # Expecting to find a URI here
    proxy_ignore_headers Cache-Control Expires Set-Cookie; # Does not influence caching
}
location @do_oidc_flow {
    auth_jwt off;
    status_zone "OIDC start";
    js_content oidc.auth;
    default_type text/plain; # In case we throw an error
}
set $redir_location "/_codexch";
location = /_codexch {
    # This location is called by the IdP after successful authentication
    auth_jwt off;
    status_zone "OIDC code exchange";
    js_content oidc.codeExchange;
    error_page 500 502 504 @oidc_error;
}
location = /_token {
    # This location is called by oidcCodeExchange(). We use the proxy_ directives
    # to construct the OpenID Connect token request, as per:
    #  http://openid.net/specs/openid-connect-core-1_0.html#TokenRequest
    auth_jwt off;
    internal;
    proxy_ssl_server_name on; # For SNI to the IdP
    proxy_set_header      Content-Type "application/x-www-form-urlencoded";
    proxy_set_header Accept-Encoding "gzip"; # fixed OIDC authorization code sent but token response is not JSON
    proxy_set_body        "grant_type=authorization_code&client_id=$oidc_client&$args&redirect_uri=$redirect_base$redir_location";
    proxy_method          POST;
    proxy_pass            $oidc_token_endpoint;
}
location = /_refresh {
    # This location is called by oidcAuth() when performing a token refresh. We
    # use the proxy_ directives to construct the OpenID Connect token request, as per:
    #  https://openid.net/specs/openid-connect-core-1_0.html#RefreshingAccessToken
    auth_jwt off;
    internal;
    proxy_ssl_server_name on; # For SNI to the IdP
    proxy_set_header      Content-Type "application/x-www-form-urlencoded";
    proxy_set_body        "grant_type=refresh_token&refresh_token=$arg_token&client_id=$oidc_client&client_secret=$oidc_client_secret";
    proxy_method          POST;
    proxy_pass            $oidc_token_endpoint;
}
location = /_id_token_validation {
    # This location is called by oidcCodeExchange() and oidcRefreshRequest(). We use
    # the auth_jwt_module to validate the OpenID Connect token response, as per:
    #  https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
    internal;
    auth_jwt "" token=$arg_token;
    js_content oidc.validateIdToken;
    error_page 500 502 504 @oidc_error;
}
location = /logout {
    auth_jwt off;
    status_zone "OIDC logout";
    add_header Set-Cookie "auth_token=; $oidc_cookie_flags"; # Send empty cookie
    add_header Set-Cookie "auth_redir=; $oidc_cookie_flags"; # Erase original cookie
    js_content oidc.logout;
}
location = /_logout {
    # This location is the default value of $oidc_logout_redirect (in case it wasn't configured)
    auth_jwt off;
    default_type text/plain;
    return 200 "Logged out\n";
}
location @oidc_error {
    # This location is called when oidcAuth() or oidcCodeExchange() returns an error
    auth_jwt off;
    status_zone "OIDC error";
    default_type text/plain;
    return 500 $internal_error_message;
}

# openid.conf

# OpenID Connect configuration
#
# Each map block allows multiple values so that multiple IdPs can be supported,
# the $host variable is used as the default input parameter but can be changed.
#
map $host $oidc_authz_endpoint {
    default "https://keycloak/realms/main/protocol/openid-connect/auth";
    #www.example.com "https://my-idp/oauth2/v1/authorize";
}

map $host $oidc_authz_extra_args {
    # Extra arguments to include in the request to the IdP's authorization
    # endpoint.
    # Some IdPs provide extended capabilities controlled by extra arguments,
    # for example Keycloak can select an IdP to delegate to via the
    # "kc_idp_hint" argument.
    # Arguments must be expressed as query string parameters and URL-encoded
    # if required.
    default "";
    #www.example.com "kc_idp_hint=another_provider"
}

map $host $oidc_token_endpoint {
    default "https://keycloak/realms/main/protocol/openid-connect/token";
}

map $host $oidc_jwt_keyfile {
    default "https://keycloak/realms/main/protocol/openid-connect/certs";
}

map $host $oidc_client {
    default "nginx-openid";
}

map $host $oidc_pkce_enable {
    default 0;
}

map $host $oidc_client_secret {
    default "secret";
}

map $host $oidc_scopes {
    default "openid+profile+email+offline_access";
}

map $host $oidc_logout_redirect {
    # Where to send browser after requesting /logout location. This can be
    # replaced with a custom logout page, or complete URL.
    default "/_logout"; # Built-in, simple logout page
}

map $host $oidc_hmac_key {
    # This should be unique for every NGINX instance/cluster
    default "key";
}

map $host $zone_sync_leeway {
    # Specifies the maximum timeout for synchronizing ID tokens between cluster
    # nodes when you use shared memory zone content sync. This option is only
    # recommended for scenarios where cluster nodes can randomly process
    # requests from user agents and there may be a situation where node "A"
    # successfully received a token, and node "B" receives the next request in
    # less than zone_sync_interval.
    default 0; # Time in milliseconds, e.g. (zone_sync_interval * 2 * 1000)
}

map $proto $oidc_cookie_flags {
    http  "Path=/; SameSite=lax;"; # For HTTP/plaintext testing
    https "Path=/; SameSite=lax; Max-Age=86400; HttpOnly; Secure;"; # Production recommendation
}

#map $http_x_forwarded_port $redirect_base {
#    ""      $proto://$host:$server_port;
#    default $proto://$host:$http_x_forwarded_port;
#}
map $http_x_forwarded_port $redirect_base {
    default $proto://$host;
}

map $http_x_forwarded_proto $proto {
    ""      $scheme;
    default $http_x_forwarded_proto;
}

# ADVANCED CONFIGURATION BELOW THIS LINE
# Additional advanced configuration (server context) in openid_connect.server_conf

# JWK Set will be fetched from $oidc_jwks_uri and cached here - ensure writable by nginx user
proxy_cache_path /var/cache/nginx/jwk levels=1 keys_zone=jwk:100m max_size=512m;

# Change timeout values to at least the validity period of each token type
keyval_zone zone=oidc_id_tokens:100M timeout=4h;
keyval_zone zone=oidc_access_tokens:100M timeout=4h;
keyval_zone zone=refresh_tokens:10M timeout=1d;
keyval_zone zone=oidc_pkce:512K; # Temporary storage for PKCE code verifier.

keyval $cookie_auth_token $session_jwt   zone=oidc_id_tokens;     # Exchange cookie for JWT
keyval $cookie_auth_token $access_token  zone=oidc_access_tokens; # Exchange cookie for access token
keyval $cookie_auth_token $refresh_token zone=refresh_tokens;     # Exchange cookie for refresh token
keyval $request_id $new_session          zone=oidc_id_tokens;     # For initial session creation
keyval $request_id $new_access_token     zone=oidc_access_tokens;
keyval $request_id $new_refresh          zone=refresh_tokens; # ''
keyval $pkce_id $pkce_code_verifier      zone=oidc_pkce;

auth_jwt_claim_set $jwt_audience aud; # In case aud is an array
js_import oidc from js/openid_connect.js;


and unmodified js/openid_connect.js; from main branch

@lcrilly
Copy link
Contributor

lcrilly commented Nov 29, 2023

Something probably broken with the refresh process. Check the error log for OIDC refresh failure messages.

@dolgovas
Copy link
Author

dolgovas commented Nov 29, 2023

2023/11/29 13:27:59 [info] 2722823#2722823: *603571 expired JWT token while sending to client, client: 192.168.10.15, server: main.example.com, request: "GET /service", host: "main.example.com", referrer: "https://main.example.com/"
2023/11/29 13:27:59 [info] 2722823#2722823: *603571 expired JWT token while sending to client, client: 192.168.10.15, server: main.example.com, request: "GET /service", host: "main.example.com", referrer: "https://main.example.com/"
2023/11/29 13:27:59 [info] 2722823#2722823: *603571 expired JWT token while sending to client, client: 192.168.10.15, server: main.example.com, request: "GET /service", host: "main.example.com", referrer: "https://main.example.com/"
2023/11/29 13:27:59 [info] 2722823#2722823: *603571 expired JWT token while sending to client, client: 192.168.10.15, server: main.example.com, request: "GET /service", host: "main.example.com", referrer: "https://main.example.com/"
2023/11/29 13:28:03 [info] 2722823#2722823: *603571 expired JWT token while sending to client, client: 192.168.10.15, server: main.example.com, request: "GET /service", host: "main.example.com", referrer: "https://main.example.com/"

only this in debug error.log

It seems like nginx cannot refresh access token... but why? Where I can get additional logs? May be it's possible to run njs script with additional output?

@dolgovas
Copy link
Author

One more thing. If timeout for keyval zone is less than ttl acess token - infinite loop starts before expiration access token, right after remove token from nginx_kv

@dolgovas
Copy link
Author

dolgovas commented Dec 8, 2023

I think I fixed this.
First one I needed to enable refresh tokens in keycloak, because in latest version it disabled by default!
Second one I added
proxy_set_header Accept-Encoding "gzip";
only into /_jwks_uri and /_token, like said in to-do troubleshooting. BUT it seems that I need to add gzip also in /_refresh location.

After these changes everything working correctly

@lcrilly
Copy link
Contributor

lcrilly commented Dec 8, 2023

Good news. Looks like the troubleshooting guide needs an extra item!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants