Gitlab 16.x CAS replacement

Gitlab 16.x removing omniauth-cas

Hey there! Im currently running a private Gitlab Instance running the latest 15.x release. I prepared myself for the 16.x upgrade and i realized that CAS will be gone in 16.x.

We’r running a custom authorization server and we’r utilising CAS3 for nearly anything we can. Now, before the update is released, we want to adapt our service to provide another way of authorization for the gitlab instance.

I’ve taken a look into the omniauth documentation and found the “JWT”-Provider. Looks like a simple way to adapt our authorization server but there is no documentation for this. (At least i’ve not found any)

CAS was documented well and was easy to implement (client and server side) and its a bummer it will be gone by 16.x

I tried a request-inspector to check what the redirect to the auth_url looks like, but it seems to be an simple redirect with no information whatsoever:

  {
    "name" => "jwt",
    "label" => "Single Sign-On (Beta)",
    "args" => {
      "secret" => "",
      "algorithm" => "HS256",
      "uid_claim" => "email",
      "required_claims" => ["name", "email"],
      "info_map" => {
        "name" => "name",
        "email" => "email"
      },
      "auth_url" => "https://requestinspector.com/inspect/01gzzr9pv2pw2yv92jm9g6teb3",
      "valid_within" => 3600
    }
  }

tl;dr: I’d like to know how the JWT provider works so we can implement it. Or maybe there is another simple authorization workflow we can implement in our authorization server.

Information about our instance:

  • 15.11.2-ee
  • Dockerized
  • Dedicated Root Server, self managed

Thanks :blush:

Hey, just a heads-up. I’ve found out the solution my self :smiley:

There is no detailed explanation but the trick is to send the user back to the callback for jwt authentication with a jwt-query param which contains a jwt token.

My configuration:

gitlab_rails['omniauth_providers'] = [
  {
      "name"=> "cas3",
      "label"=> "Single Sign-On",
      "args"=> {
          "url"=> 'https://authy.xxxx.de',
          "login_url"=> '/cas/login',
          "service_validate_url"=> '/cas/validate',
          "logout_url"=> '/cas/logout'
      }
  },
  {
    "name" => "jwt",
    "label" => "Single Sign-On (Beta)",
    "args" => {
      "secret" => "xxxxxx",
      "algorithm" => "HS512",
      "uid_claim" => "email",
      "required_claims" => ["sub", "email"],
      "info_map" => {
        "sub" => "name",
        "email" => "email"
      },
      "auth_url" => "https://authy.xxxx.de/jwt/login?service=https://git.xxxx.de/users/auth/jwt/callback",
      "valid_within" => 3600
    }
  }
]

The java code (does not work out of the box but should tell you enough to implement on your own)


@Slf4j
@RestController
@RequiredArgsConstructor
public class JwtAuthResourceImpl implements JwtAuthResource {
    public ResponseEntity<LoginResponse> login(HttpServletRequest req, HttpServletResponse response, LoginRequest login, String serviceUrl) {

        Tuple2<Identity, Service> authenticated = loginSecurity.authenticate(req, login, serviceUrl);

        Service  service  = authenticated.getT2();
        Identity identity = authenticated.getT1();

        String token = issueCookie(response, identity, service);

        return ResponseEntity.ok(
                LoginResponse.builder()
                        .token(token)
                        .location(login.getCas() ? getRedirectLogin(serviceUrl, identity, service) : serviceUrl)
                        .message("OK")
                        .build()
        );
    }

    @Override
    public ResponseEntity<LoginResponse> loginPage(HttpServletRequest request, String serviceUrl) {
        if (SecureContextRequestHelper.hasSecureContext(request)) {
            SecureContext ctx = SecureContextRequestHelper.getSecureContext(request);
            assert ctx != null;

            Identity identity = ctx.getIdentity();
            Service  service  = serviceValidation.getRegisteredServiceFor(serviceUrl);

            if (service != null && service.getEnabled()) {
                if (service.isIdentityAllowed(identity)) {
                    String redirectUrl = getRedirectLogin(serviceUrl, identity, service);
                    return ResponseEntity.status(HttpStatus.TEMPORARY_REDIRECT)
                            .header("Location", redirectUrl)
                            .body(LoginResponse.builder().location(redirectUrl).message("OK").build());
                } else {
                    return ResponseEntity.status(HttpStatus.TEMPORARY_REDIRECT).location(URI.create("/#/error?code=" + StatusCode.DENIED + "&service=" + serviceUrl + "&require2FA=" + service.getRequire2FA().toString())).build();
                }
            }
        }

        Service service = serviceValidation.getRegisteredServiceFor(serviceUrl);

        if (service == null || !service.getEnabled()) {
            return ResponseEntity.status(HttpStatus.TEMPORARY_REDIRECT).location(URI.create("/#/error?code=" + StatusCode.INVALID_SERVICE + "&service=" + serviceUrl)).build();
        }

        if (serviceUrl.equals("/")) {
            return ResponseEntity.status(HttpStatus.TEMPORARY_REDIRECT).location(URI.create("/#/login?service=" + serviceUrl)).build();
        } else {
            return ResponseEntity.status(HttpStatus.TEMPORARY_REDIRECT).location(URI.create("/#/jwt/login?service=" + serviceUrl)).build();
        }
    }

    private String getRedirectLogin(String serviceUrl, Identity identity, Service service) {
        if (serviceUrl.contains("?")) {
            serviceUrl += "&";
        } else {
            serviceUrl += "?";
        }

        serviceUrl += "jwt=" + jwtProcessor.getJwtTokenFor(identity, service); // important part
        return serviceUrl;
    }
}