Skip to content

Commit

Permalink
Token verification, cleanup and documentation
Browse files Browse the repository at this point in the history
Using keycloaks JWK endpoint to get public key from the server, beeing
able to verify the JWTs signature. Also check the token for expiration
and deny request if token is expired.

Cleanup code and remove unneccessary/unused stuff. Update documentation
to reflect changes made.
  • Loading branch information
pschmidt88 committed May 12, 2020
1 parent c59caba commit b0e5d5b
Show file tree
Hide file tree
Showing 28 changed files with 304 additions and 863 deletions.
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
build/
.vertx
.idea
.gradle
31 changes: 8 additions & 23 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,39 +1,24 @@
# -- build
FROM openjdk:8-jdk-slim AS builder
FROM gradle:jdk8 AS builder
COPY ./ /src/
WORKDIR /src/
RUN ./gradlew --info --no-daemon build
RUN gradle --debug --no-daemon shadowJar

# -- run
FROM alpine:3.10
FROM openjdk:8-jre-alpine

# Install java runtime
RUN apk add --no-cache --update openjdk8-jre && \
rm -rf /tmp/* /var/cache/apk/*

ENV JAVA_HOME=/usr/lib/jvm/default-jvm/jre
ENV JAVA_TOOL_OPTIONS ""
ENV ALLOWED_USER_AGENTS_ON_ROOT_REGEX "GoogleHC"
ENV AUTH_CACHE_TTL "300"
ENV BIND_PORT "8080"
ENV BIND_PORT "80"
ENV CLIENT_ID "REPLACE_ME"
ENV CLIENT_SECRET "REPLACE_ME"
ENV CLOUD_IAM_AUTH_ENABLED "true"
ENV JWT_REQUIRES_MEMBERSHIP_VERIFICATION "true"
ENV KEYSTORE_PATH "keystore.jceks"
ENV KEYSTORE_PASS "safe#passw0rd!"
ENV NEXUS_DOCKER_HOST "containers.example.com"
ENV NEXUS_HTTP_HOST "nexus.example.com"
ENV NEXUS_RUT_HEADER "X-Forwarded-User"
ENV ORGANIZATION_ID "REPLACE_ME"
ENV REDIRECT_URL "https://nexus.example.com/oauth/callback"
ENV REDIRECT_URL "<oauth-callback>"
ENV SESSION_TTL "1440000"
ENV TLS_CERT_PK12_PATH "cert.pk12"
ENV TLS_CERT_PK12_PASS "safe#passw0rd!"
ENV TLS_ENABLED "false"
ENV UPSTREAM_DOCKER_PORT "5003"
ENV UPSTREAM_HOST "localhost"
ENV UPSTREAM_HTTP_PORT "8081"
ENV JWK_URL "<openid-certs>"
ENV TOKEN_ENDPOINT "<openid-token-endpoint>"
ENV AUTHORIZE_ENDPOINT "<openid-authorize-url>"

COPY --from=builder /src/build/libs/nexus-proxy-2.3.0.jar /nexus-proxy.jar

Expand Down
162 changes: 22 additions & 140 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,66 +1,10 @@
# nexus-proxy

[![Build Status](https://travis-ci.org/travelaudience/nexus-proxy.svg?branch=master)](https://travis-ci.org/travelaudience/nexus-proxy)
[![Docker Repository on Quay](https://quay.io/repository/travelaudience/docker-nexus-proxy/status "Docker Repository on Quay")](https://quay.io/repository/travelaudience/docker-nexus-proxy)
_This project is a fork of the original [nexus-proxy (by travelaudience)](https://github.com/travelaudience/nexus-proxy) which allowed optional authentication
against the Google Cloud IAM. It is not hard-wired to Google Cloud IAM anymore. You can use it with any IDP which supports the OAuth2 Code Authorization Flow.
Furthermore the docker proxy was removed since we only needed authentication for npm and gradle/maven._

A proxy for Nexus Repository Manager that allows for optional authentication against external identity providers.

Read [the design document](docs/design.md) for a more detailed explanation of why and how.

## Before proceeding

**ATTENTION**: This software does not manage or enforce authorization. It's
therefore required that users, roles and permissions are to be configured
through Nexus administrative UI before start using Nexus.

**ATTENTION**: If GCP IAM authentication is enabled, every user account
**must be created** with their organization email address as the username.
A password needs to be set but it will only be important if GCP IAM
authentication is disabled. **Also** it is necessary to grant the
"_Organization Viewer_" role [**at organization-level**](https://cloud.google.com/iam/docs/resource-hierarchy-access-control)
(i.e., in the "_IAM & Admin_" section of the organization in the GCP UI) to
every user.

**ATTENTION:**: If GCP IAM authentication is enabled, it is necessary to
[enable the Nexus "_Rut Auth_" capability](https://help.sonatype.com/display/NXRM3/Security#Security-AuthenticationviaRemoteUserToken).
Otherwise, authentication succeeds but Nexus can't initiate user sessions.

**ATTENTION**: The Nexus-specific credentials mentioned above are valid for
one year **and** for as long as the user is a member of the GCP organization.

**ATTENTION**: If the `ENFORCE_HTTPS` flag is set to `true` it is assumed that
one has configured `nexus-proxy` or any load-balancers in front of it to serve
HTTPS on host `NEXUS_HTTP_HOST` and port `443` with a valid TLS certificate.

**ATTENTION:**: Setting the `JWT_REQUIRES_MEMBERSHIP_VERIFICATION` environment variable to `false` inherently makes `nexus-proxy` less secure.
In this scenario, a user containing a valid JWT token will be able to make requests using CLI tools like Maven or Docker without having to go through the OAuth2 consent screen.
For example, if a user leaves the organization while keeping a valid JWT token, and this environment variable is set to `false`, they will still be able to make requests to Nexus.

## Introduction

While deploying Nexus Repository Manager on GKE, we identified a couple issues:

1. GCLB backend health-checks weren't working when reaching Nexus directly.
1. Couldn't expose Docker private registry with the same set-up used to
expose the other artifact repositories.

We also knew beforehand that we would need to authenticate Nexus against
[Google Cloud Identity & Access Management](https://cloud.google.com/iam/).

While the aforementioned issues were easily fixed with [Nginx](https://nginx.org/en/),
the authentication part proved much more complicated. For all of those reasons,
we decided to implement our own proxy software that would deliver everything we
needed.

Also, authentication is disabled by default so it can be used in simpler scenarios.

**When GCP IAM authentication is enabled**, every user attempting to access Nexus
is asked to authenticate against GCP with their GCP organization credentials.
If authentication succeeds, an encrypted token will be generated by the proxy
and sent to the client ,e.g. browser, so it knows how to authenticate itself.
After being logged-in, and only when authentication is enabled, the user must
request Nexus-specific credentials for using with tools like Maven,
Gradle, sbt, Python (pip) and Docker.
A proxy for Nexus Repository Manager that allows for optional authentication against an external identity provider (which implements OAuth2/OpenID).

## Pre-requisites

Expand All @@ -73,116 +17,54 @@ For basic proxying:
* A domain name configured with an `A` and a `CNAME` records pointing to the proxy.
* For local testing one may create two entries on `/etc/hosts` pointing to `127.0.0.1`.
* A running and properly configured instance of Nexus.
* One may use the default `8081` port for the HTTP connector and `5003` for the Docker registry, for example.
* One may use the default `8081` port for the HTTP connector.

For opt-in authentication against Google Cloud IAM:
For opt-in authentication against an IDP:

* All of the above.
* A GCP organization.
* A GCP project with the _Cloud Resources Manager_ API enabled.
* A set of credentials of type _OAuth Client ID_ obtained from _GCP > API Manager > Credentials_.
* Proper configuration of the resulting client's "_Redirect URL_".

## Generating the Keystore

A Java keystore is needed in order for the proxy to sign user tokens (JWT).
Here's how to generate the keystore:

```bash
$ keytool -genkey \
-keystore keystore.jceks \
-storetype jceks \
-keyalg RSA \
-keysize 2048 \
-alias RS256 \
-sigalg SHA256withRSA \
-dname "CN=,OU=,O=,L=,ST=,C=" \
-validity 3651
```

One will be prompted for two passwords. One must make sure the passwords match.

Also, one is free to change the value of the `dname`, `keystore` and `validity` parameters.

## Building the code

The following command will build the project and generate a runnable jar:

```bash
$ ./gradlew build
```
* A properly configured IDP, e.g. Keycloak
* A set of credentials (`CLIENT_ID` & `CLIENT_SECRET`)
* OAuth2 Endpoint URLs (`AUTHORIZE_ENDPOINT`, `TOKEN_ENDPOINT`, `JWK_URL`)
* Proper configuration of the resulting client's `REDIRECT_URL`.

## Running the proxy

The following command will run the proxy on port `8080` with no authentication
and pointing to a local Nexus instance:

```bash
$ ALLOWED_USER_AGENTS_ON_ROOT_REGEX="GoogleHC" \
BIND_PORT="8080" \
NEXUS_DOCKER_HOST="containers.example.com" \
NEXUS_HTTP_HOST="nexus.example.com" \
NEXUS_RUT_HEADER="X-Forwarded-User" \
TLS_ENABLED="false" \
UPSTREAM_DOCKER_PORT="5000" \
UPSTREAM_HTTP_PORT="8081" \
UPSTREAM_HOST="localhost" \
java -jar ./build/libs/nexus-proxy-2.3.0.jar
```

## Running the proxy with GCP IAM authentication enabled
## Running the proxy with OpenID Authentication

The following command will run the proxy on port `8080` with GCP IAM
authentication enabled and pointing to a local Nexus instance:
The following command will run the proxy on port `8080` with
authentication against an OpenID IDP enabled and pointing to a local Nexus instance:

```bash
$ ALLOWED_USER_AGENTS_ON_ROOT_REGEX="GoogleHC" \
AUTH_CACHE_TTL="60000" \
BIND_PORT="8080" \
$ BIND_PORT="8080" \
CLOUD_IAM_AUTH_ENABLED="true" \
CLIENT_ID="my-client-id" \
CLIENT_SECRET="my-client-secret" \
KEYSTORE_PATH="./.secrets/keystore.jceks" \
KEYSTORE_PASS="my-keystore-password" \
NEXUS_DOCKER_HOST="containers.example.com" \
NEXUS_HTTP_HOST="nexus.example.com" \
NEXUS_RUT_HEADER="X-Forwarded-User" \
ORGANIZATION_ID="123412341234" \
REDIRECT_URL="https://nexus.example.com/oauth/callback" \
SESSION_TTL="1440000" \
TLS_ENABLED="false" \
UPSTREAM_DOCKER_PORT="5000" \
UPSTREAM_HTTP_PORT="8081" \
UPSTREAM_HOST="localhost" \
TOKEN_ENDPOINT="https://<sso-base-url>/openid-connect/token" \
JWK_URL="https://<sso-base-url>/openid-connect/certs" \
TOKEN_ENDPOINT="https://<sso-base-url>/openid-connect/auth" \
java -jar ./build/libs/nexus-proxy-2.3.0.jar
```

## Environment Variables

| Name | Description |
|-------------------------------------|-------------|
| `ALLOWED_USER_AGENTS_ON_ROOT_REGEX` | A regex against which to match the `User-Agent` of requests to `GET /` so that they can be answered with `200 OK`. |
| `AUTH_CACHE_TTL` | The amount of time (in _milliseconds_) during which to cache the fact that a given user is authorized to make requests. |
| `BIND_HOST` | The interface on which to listen for incoming requests. Defaults to `0.0.0.0`. |
| `BIND_PORT` | The port on which to listen for incoming requests. |
| `CLIENT_ID` | The application's client ID in _GCP / API Manager / Credentials_. |
| `CLIENT_ID` | The application's OAuth2 client ID|
| `CLIENT_SECRET` | The abovementioned application's client secret. |
| `CLOUD_IAM_AUTH_ENABLED` | Whether to enable authentication against Google Cloud IAM. |
| `ENFORCE_HTTPS` | Whether to enforce access by HTTPS only. If set to `true` Nexus will only be accessible via HTTPS. |
| `JAVA_TOOL_OPTIONS` | JVM options to provide, for example `-XX:MaxDirectMemorySize=1024M`. |
| `JWT_REQUIRES_MEMBERSHIP_VERIFICATION` | Whether users presenting valid JWT tokens must still be verified for membership within the organization. |
| `KEYSTORE_PATH` | The path to the keystore containing the key with which to sign JWTs. |
| `KEYSTORE_PASS` | The password of the abovementioned keystore. |
| `CLOUD_IAM_AUTH_ENABLED` | Whether to enable authentication against an IDP. |
| `LOG_LEVEL` | The desired log level (i.e., `trace`, `debug`, `info`, `warn` or `error`). Defaults to `info`. |
| `NEXUS_DOCKER_HOST` | The host used to access the Nexus Docker registry. |
| `NEXUS_HTTP_HOST` | The host used to access the Nexus UI and Maven repositories. |
| `NEXUS_RUT_HEADER` | The name of the header which will convey auth info to Nexus. |
| `ORGANIZATION_ID` | The ID of the organization against which to validate users' membership. |
| `REDIRECT_URL` | The URL where to redirect users after the OAuth2 consent screen. |
| `SESSION_TTL` | The TTL (in _milliseconds_) of a user's session. |
| `TLS_CERT_PK12_PATH` | The path to the PK12 file to use when enabling TLS. |
| `TLS_CERT_PK12_PASS` | The password of the PK12 file to use when enabling TLS. |
| `TLS_ENABLED` | Whether to enable TLS. |
| `UPSTREAM_DOCKER_PORT` | The port where the proxied Nexus Docker registry listens. |
| `UPSTREAM_HTTP_PORT` | The port where the proxied Nexus instance listens. |
| `UPSTREAM_HOST` | The host where the proxied Nexus instance listens. |
| `AUTHORIZE_ENDPOINT` | The OAuth2/OpenID auth endpoint for the Authorize Flow |
| `TOKEN_ENDPOINT` | The OAuth2/OpenID token endpoint for the Authorize Flow |
| `JWK_URL` | URL where the server can receive the IDP's JWK. Needed for verifying the tokens signature. |
39 changes: 4 additions & 35 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
plugins {
id 'java'
id 'application'
id 'com.github.johnrengelman.shadow' version '2.0.1'
id "com.diffplug.gradle.spotless" version "3.29.0"
id 'com.github.johnrengelman.shadow' version '5.2.0'
}

group 'com.travelaudience.nexus'
Expand All @@ -13,50 +12,20 @@ sourceCompatibility = 1.8
targetCompatibility = 1.8

dependencies {
implementation 'com.auth0:java-jwt:3.10.3'
implementation 'com.auth0:jwks-rsa:0.9.0'

compile 'ch.qos.logback:logback-classic:1.2.3'
compile 'com.github.ben-manes.caffeine:caffeine:2.5.2'
compile 'com.google.apis:google-api-services-cloudresourcemanager:v1beta1-rev446-1.22.0'
compile 'com.google.apis:google-api-services-oauth2:v1-rev127-1.22.0'
compile 'io.vertx:vertx-auth-jwt:3.4.2'
compile 'io.vertx:vertx-unit:3.4.2'
compile 'io.vertx:vertx-web:3.4.2'
compile 'io.vertx:vertx-web-templ-handlebars:3.4.2'
testCompile 'org.powermock:powermock-api-mockito2:1.7.0'
testCompile 'org.powermock:powermock-module-junit4:1.7.0'
}

repositories {
mavenCentral()
}

task sourcesJar(type: Jar, dependsOn: classes) {
classifier = 'sources'
from sourceSets.main.allSource
}

task javadocJar(type: Jar, dependsOn: javadoc) {
classifier = 'javadoc'
from javadoc.destinationDir
javadoc.failOnError = false
}

artifacts {
archives sourcesJar
archives javadocJar
}

test {
// the following are not needed until e2e tests are made available
systemProperty "ORGANIZATION_ID", "ORGANIZATION_ID"
systemProperty "CLIENT_ID", "CLIENT_ID"
systemProperty "CLIENT_SECRET", "CLIENT_SECRET"
}

shadowJar {
classifier = null
}

task wrapper(type: Wrapper) {
distributionUrl = "https://services.gradle.org/distributions/gradle-$gradleVersion-all.zip"
gradleVersion = '4.9.0'
}
Loading

0 comments on commit b0e5d5b

Please sign in to comment.