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

Add param converters for authentication credentials #1374

Merged
merged 5 commits into from Jan 6, 2020

Conversation

pkoenig10
Copy link
Member

@pkoenig10 pkoenig10 commented Dec 12, 2019

Before this PR

Malformed authentication credentials will result in a 400. This is unfortunate because it will propagate as a 500.

After this PR

Malformed authentication credentials will result in a 401.

This change is the equivalent to part of palantir/conjure-java#656 for Jersey servers.

@changelog-app
Copy link

changelog-app bot commented Dec 12, 2019

Generate changelog in changelog/@unreleased

Type

  • Feature
  • Improvement
  • Fix
  • Break
  • Deprecation
  • Manual task
  • Migration

Description

Missing or malformed credentials will result in a 401 UNAUTHORIZED from Jersey servers. Previously we responded with a 400, which isn't as accurate as we would like.

Check the box to generate changelog(s)

  • Generate changelog entry

@Override
public AuthHeader fromString(final String value) {
if (value == null) {
throw new UnauthorizedException(MISSING_CREDENTIAL_ERROR_TYPE);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be a WebApplicationException. Otherwise it will get wrapped in a ExtractorException which will get mapped to a 400:
https://github.com/jersey/jersey/blob/2.25.1/core-server/src/main/java/org/glassfish/jersey/server/internal/inject/AbstractParamValueExtractor.java#L137

The jaxrs NotAuthorizedException doesn't provide constructors that can be used in this scenario so we need to create our own. I chose the name UnauthorizedException to disambiguate.

import org.glassfish.jersey.internal.inject.Providers;
import org.glassfish.jersey.internal.util.ReflectionHelper;
import org.glassfish.jersey.internal.util.collection.ClassTypePair;

@Custom
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This annotation ensures that our custom param converters are considered first.
Otherwise the order in which the param converter providers are considered is arbitrary:
https://github.com/jersey/jersey/blob/2.25.1/core-server/src/main/java/org/glassfish/jersey/server/internal/inject/ParamConverterFactory.java#L58-L101

For example, here is an order that was used when I was debugging tests locally:
param-converter-providers

This is especially important for the AuthHeaderParamConverterProvider and BearerTokenParamConverterProvider to ensure that we don't fallback to the built-in AggregatedProvider which contains the TypeValueOf param converter (which is used currently for AuthHeader and BearerToken).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something along these lines would make an excellent comment in the code :-)

@pkoenig10 pkoenig10 changed the title Add param converters for authentication Add param converters for authentication credentials Dec 12, 2019
@pkoenig10 pkoenig10 force-pushed the pkoenig10/authParamConverter branch 3 times, most recently from 8fea8cd to 17ea493 Compare December 13, 2019 00:48

@Custom
@Provider
public final class BearerTokenParamConverterProvider implements ParamConverterProvider {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not confident this is correct, while cookie auth uses the BearerToken type, we can define additional parameters of type BearerToken that aren't the conjure-defined auth token.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mention this in the PR description:

These param converters will apply to all AuthHeader and BearerToken parameters. However it is very rare for these types to be used outside of the endpoint auth. And most of those cases are to perform some sort of delegate authentication, so a 401 is not inappropriate.

I don't think I've ever seen a BearerToken used as an argument outside of cookie auth. And the only times I've seen a AuthHeader used as argument outside of header auth is for delegating authorization, in which case you could argue that a 401 is not inappropriate.

I don't think there is another way to achieve the desired behavior. If you have other suggestions, I'm happy to hear them. But I think the benefit of responding with 401 for invalid auth credentials outweighs the minor downside of this applying to other parameters of the same type.

Copy link
Member Author

@pkoenig10 pkoenig10 Dec 18, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated this PR to only apply to Conjure auth parameters. This means:

  • AuthHeader parameter must be annotated with @HeaderParam and have a value of Authorization
  • BearerToken parameter must be annotated with @CookieParam and have any value

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds reasonable, I don't think we need the additional specificity for AuthHeader because conjure only uses that type for auth components, bearer tokens use the BearerToken type.

Copy link
Member Author

@pkoenig10 pkoenig10 Dec 18, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to not respond with 401 for parameters we know aren't the Conjure auth parameter. It's possible to have other AuthHeader header params by using external imports. We actually do this for a number of endpoints and use a second AuthHeader header param to represent a delegating user. It makes more sense to return 400 for these parameters since they are effectively arguments that just happen to be passed as headers.

Because Conjure doesn't allow arbitrary cookie parameters, this implementation will exactly match all auth parameters and nothing else.

@stale
Copy link

stale bot commented Jan 1, 2020

This PR has been automatically marked as stale because it has not been touched in the last 14 days. If you'd like to keep it open, please leave a comment or add the 'long-lived' label, otherwise it'll be closed in 7 days.

@stale stale bot added the stale label Jan 1, 2020
@pkoenig10
Copy link
Member Author

@carterkozak are there any other changes we should make in this PR to try to gracefully handle applications that rely on tokens being nullable?

@stale stale bot removed the stale label Jan 2, 2020
@carterkozak
Copy link
Contributor

Let's add test coverage to make sure this change doesn't modify null token behavior, this way we can decouple the risky piece from the type of exception thrown from our infrastructure. We can hold off on null handling until the office opens up and we can discuss more broadly :-)

Copy link
Contributor

@carterkozak carterkozak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally looks good to me, thanks! Just a couple small nits


private static boolean hasAuthAnnotation(Annotation[] annotations) {
for (Annotation annotation : annotations) {
if (annotation.annotationType() == CookieParam.class) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
if (annotation.annotationType() == CookieParam.class) {
if (CookieParam.class.equals(annotation.annotationType())) {


private static boolean hasAuthAnnotation(Annotation[] annotations) {
for (Annotation annotation : annotations) {
if (annotation.annotationType() == HeaderParam.class) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
if (annotation.annotationType() == HeaderParam.class) {
if (HeaderParam.class.equals(annotation.annotationType())) {

for (Annotation annotation : annotations) {
if (annotation.annotationType() == HeaderParam.class) {
String value = ((HeaderParam) annotation).value();
if (value.equals(HttpHeaders.AUTHORIZATION)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, safer around nulls:

Suggested change
if (value.equals(HttpHeaders.AUTHORIZATION)) {
if (HttpHeaders.AUTHORIZATION.equals(value)) {

import org.glassfish.jersey.internal.inject.Providers;
import org.glassfish.jersey.internal.util.ReflectionHelper;
import org.glassfish.jersey.internal.util.collection.ClassTypePair;

@Custom
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something along these lines would make an excellent comment in the code :-)

@carterkozak
Copy link
Contributor

Thanks! 👍

@bulldozer-bot bulldozer-bot bot merged commit 1ea97f0 into develop Jan 6, 2020
@bulldozer-bot bulldozer-bot bot deleted the pkoenig10/authParamConverter branch January 6, 2020 19:15
@svc-autorelease
Copy link
Collaborator

Released 4.47.0

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

Successfully merging this pull request may close these issues.

None yet

3 participants