org.apereo.portal.rest.oauth.OidcUserInfoController.java Source code

Java tutorial

Introduction

Here is the source code for org.apereo.portal.rest.oauth.OidcUserInfoController.java

Source

/**
 * Licensed to Apereo under one or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information regarding copyright ownership. Apereo
 * licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use
 * this file except in compliance with the License. You may obtain a copy of the License at the
 * following location:
 *
 * <p>http://www.apache.org/licenses/LICENSE-2.0
 *
 * <p>Unless required by applicable law or agreed to in writing, software distributed under the
 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apereo.portal.rest.oauth;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.apereo.portal.security.IPerson;
import org.apereo.portal.security.IPersonManager;
import org.apereo.portal.security.oauth.IdTokenFactory;
import org.apereo.portal.services.PersonService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * This controller provides endpoints through which clients can obtain an ID Token in a format that
 * is aligned with OpenID Connect (OIDC). <i>Clients</i> in this case are normally content objects
 * embedded within the portal page itself (e.g. portlets, soffits, or JS bundles).
 *
 * <p>It is critical to point out that this endpoint is not a compliant OIDC Identity Provider. It
 * does not implement OAuth Authentication Flows of any sort.
 *
 * <p>The value of this endpoint is not (therefore) that it brings support for OIDC to uPortal, but
 * that other modules and services designed to work with uPortal can implement security using
 * standard OIDC approaches.
 *
 * @since 5.1
 */
@RestController
public class OidcUserInfoController {

    public static final String USERINFO_ENDPOINT_URI = "/v5-1/userinfo";
    public static final String USERINFO_CONTENT_TYPE = "application/jwt";
    public static final String TOKEN_ENDPOINT_URI = "/v5-5/oauth/token";

    @Autowired
    private IPersonManager personManager;

    @Autowired
    private IdTokenFactory idTokenFactory;

    @Autowired(required = false)
    private List<OAuthClient> clientList = Collections.emptyList();

    private Map<String, OAuthClient> clientMap = Collections.emptyMap();

    @Autowired
    private PersonService personService;

    @Value("${org.apereo.portal.security.oauth.IdTokenFactory.timeoutSeconds:300}")
    private long timeoutSeconds;

    private final Logger logger = LoggerFactory.getLogger(getClass());

    @PostConstruct
    public void init() {
        final Map<String, OAuthClient> map = clientList.stream()
                .collect(Collectors.toMap(OAuthClient::getClientId, Function.identity()));
        this.clientMap = Collections.unmodifiableMap(map);
    }

    /** Obtain an OIDC Id token for the current user. */
    @RequestMapping(value = USERINFO_ENDPOINT_URI, produces = USERINFO_CONTENT_TYPE, method = { RequestMethod.GET,
            RequestMethod.POST })
    public String userInfo(HttpServletRequest request,
            @RequestParam(value = "claims", required = false) String claims,
            @RequestParam(value = "groups", required = false) String groups) {

        final IPerson person = personManager.getPerson(request);
        return createToken(person, claims, groups);
    }

    /**
     * Obtain an OIDC Id token for the specified <code>client_id</code>. At least one bean of type
     * {@link OAuthClient} is required to use this endpoint.
     *
     * <p>This token strategy supports Spring's <code>OAuth2RestTemplate</code> for accessing
     * uPortal REST APIs from external systems. Use a <code>ClientCredentialsResourceDetails</code>
     * with <code>clientAuthenticationScheme=AuthenticationScheme.form</code>, together with a
     * <code>ClientCredentialsAccessTokenProvider</code>.
     *
     * @since 5.5
     */
    @PostMapping(value = TOKEN_ENDPOINT_URI, produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity oauthToken(@RequestParam(value = "client_id") String clientId,
            @RequestParam(value = "client_secret") String clientSecret,
            @RequestParam(value = "grant_type", required = false, defaultValue = "client_credentials") String grantType,
            @RequestParam(value = "scope", required = false, defaultValue = "/all") String scope,
            @RequestParam(value = "claims", required = false) String claims,
            @RequestParam(value = "groups", required = false) String groups) {

        /*
         * NB:  Several of this method's parameters are not consumed (yet) in any way.  They are
         * defined to match a two-legged OAuth strategy and for future use.
         */

        final String msg = "Processing request for OAuth access token;  client_id='{}', client_secret='{}', "
                + "grant_type='{}', scope='{}', claims='{}', groups='{}'";
        logger.debug(msg, clientId, StringUtils.repeat("*", clientSecret.length()), grantType, scope, claims,
                groups);

        // STEP 1:  identify the client
        final OAuthClient oAuthClient = clientMap.get(clientId);
        if (oAuthClient == null) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN)
                    .body(Collections.singletonMap("message", "client_id not found"));
        }

        logger.debug("Selected known OAuthClient with client_id='{}' for access token request",
                oAuthClient.getClientId());

        // STEP 2:  validate the client_secret
        if (!oAuthClient.getClientSecret().equals(clientSecret)) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN)
                    .body(Collections.singletonMap("message", "authentication failed"));
        }

        // STEP 3:  obtain the specified user
        final IPerson person = personService.getPerson(oAuthClient.getPortalUserAccount());
        if (person == null) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Collections.singletonMap("message",
                    "portal user account not found: " + oAuthClient.getPortalUserAccount()));
        }

        logger.debug("Selected portal Person with username='{}' for client_id='{}'", person.getUserName(),
                oAuthClient.getClientId());

        // STEP 4:  build a standard OAuth2 access token response
        final String token = createToken(person, claims, groups);
        final Map<String, Object> rslt = new HashMap<>();
        rslt.put("access_token", token);
        rslt.put("token_type", "bearer");
        rslt.put("expires_in", timeoutSeconds > 2 ? timeoutSeconds - 2L /* fudge factor */ : timeoutSeconds);
        rslt.put("scope", scope);

        logger.debug("Produced the following access token for client_id='{}':  {}", oAuthClient.getClientId(),
                rslt);

        return ResponseEntity.ok(rslt);
    }

    private String createToken(IPerson person, String claims, String groups) {

        Set<String> claimsToInclude = null;
        if (claims != null) {
            String[] tokens = claims.split("[,]");
            claimsToInclude = new HashSet<>(Arrays.asList(tokens));
        }

        Set<String> groupsToInclude = null;
        if (groups != null) {
            String[] tokens = groups.split("[,]");
            groupsToInclude = new HashSet<>(Arrays.asList(tokens));
        }

        return idTokenFactory.createUserInfo(person.getUserName(), claimsToInclude, groupsToInclude);
    }
}