001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2014-2015 ForgeRock AS.
015 */
016
017package org.forgerock.openig.filter.oauth2.challenge;
018
019import static java.lang.String.format;
020import static org.forgerock.util.Reject.checkNotNull;
021
022import org.forgerock.http.Handler;
023import org.forgerock.http.protocol.Request;
024import org.forgerock.http.protocol.Response;
025import org.forgerock.services.context.Context;
026import org.forgerock.util.promise.NeverThrowsException;
027import org.forgerock.util.promise.Promise;
028import org.forgerock.util.promise.Promises;
029
030/**
031 * This handler build an authentication challenge to be returned in the {@link Response} {@literal Authorization} HTTP
032 * header.
033 * <p>
034 * It has to be sub-classed in order to create a {@link Response} with the appropriate status code and reason phrase.
035 */
036public abstract class AuthenticateChallengeHandler implements Handler {
037
038    /**
039     * Authorization HTTP Header name.
040     */
041    public static final String WWW_AUTHENTICATE = "WWW-Authenticate";
042
043    private final String realm;
044    private final String error;
045    private final String description;
046    private final String uri;
047
048    /**
049     * Creates a new AuthenticateChallengeHandler. The realm must not be {@literal null}.
050     *
051     * @param realm
052     *         mandatory realm value.
053     * @param error
054     *         error code (will be omitted if {@literal null})
055     * @param description
056     *         error description (will be omitted if {@literal null})
057     * @param uri
058     *         error uri page (will be omitted if {@literal null})
059     */
060    protected AuthenticateChallengeHandler(final String realm,
061                                           final String error,
062                                           final String description,
063                                           final String uri) {
064        this.realm = checkNotNull(realm, "OAuth2 Challenge needs a realm");
065        this.error = error;
066        this.description = description;
067        this.uri = uri;
068    }
069
070    @Override
071    public Promise<Response, NeverThrowsException> handle(final Context context, final Request request) {
072        Response response = createResponse();
073        response.getHeaders().put(WWW_AUTHENTICATE,
074                                  format("Bearer %s", buildChallenge()));
075        return Promises.newResultPromise(response);
076    }
077
078    /**
079     * Creates a {@link Response} with the appropriate status code and reason. This method is called each time the
080     * {@link #handle(Context, Request)} method is invoked.
081     *
082     * @return a new initialized {@link Response} instance
083     */
084    protected abstract Response createResponse();
085
086    private String buildChallenge() {
087        StringBuilder sb = new StringBuilder();
088
089        appendRealm(sb);
090        appendError(sb);
091        appendErrorDescription(sb);
092        appendErrorUri(sb);
093        appendExtraAttributes(sb);
094
095        return sb.toString();
096    }
097
098    private void appendRealm(final StringBuilder sb) {
099        sb.append(format("realm=\"%s\"", realm));
100    }
101
102    private void appendError(final StringBuilder sb) {
103        if (error != null) {
104            sb.append(format(", error=\"%s\"", error));
105        }
106    }
107
108    private void appendErrorDescription(final StringBuilder sb) {
109        if (description != null) {
110            sb.append(format(", error_description=\"%s\"", description));
111        }
112    }
113
114    /**
115     * Permits sub-classes to append extra attributes to the challenge.
116     * @param sb Challenge value
117     */
118    protected void appendExtraAttributes(final StringBuilder sb) {
119        // For sub-classes to add extra attributes
120    }
121
122    private void appendErrorUri(final StringBuilder sb) {
123        if (uri != null) {
124            sb.append(format(", error_uri=\"%s\"", uri));
125        }
126    }
127
128}