All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.gravitee.am.gateway.handler.ciba.service.request.CibaAuthenticationRequestResolver Maven / Gradle / Ivy

/**
 * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
 *
 * Licensed 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
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * 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 io.gravitee.am.gateway.handler.ciba.service.request;

import com.nimbusds.jwt.JWT;
import com.nimbusds.jwt.JWTParser;
import com.nimbusds.jwt.SignedJWT;
import io.gravitee.am.common.exception.jwt.ExpiredJWTException;
import io.gravitee.am.common.exception.oauth2.ExpiredLoginHintTokenException;
import io.gravitee.am.common.exception.oauth2.InvalidRequestException;
import io.gravitee.am.gateway.handler.common.jwt.SubjectManager;
import io.gravitee.am.gateway.handler.common.user.UserService;
import io.gravitee.am.gateway.handler.oauth2.service.request.AbstractRequestResolver;
import io.gravitee.am.gateway.handler.oidc.service.jwk.JWKService;
import io.gravitee.am.gateway.handler.oidc.service.jws.JWSService;
import io.gravitee.am.model.Domain;
import io.gravitee.am.model.oidc.Client;
import io.gravitee.am.repository.management.api.search.FilterCriteria;
import io.reactivex.rxjava3.core.Single;
import net.minidev.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;

import java.text.ParseException;
import java.time.Instant;
import java.util.Date;

import static io.gravitee.am.jwt.DefaultJWTParser.evaluateExp;

/**
 * @author Eric LELEU (eric.leleu at graviteesource.com)
 * @author GraviteeSource Team
 */
public class CibaAuthenticationRequestResolver extends AbstractRequestResolver {
    private static final Logger LOGGER = LoggerFactory.getLogger(CibaAuthenticationRequestResolver.class);

    private final Domain domain;
    private final JWSService jwsService;
    private final JWKService jwkService;
    private final UserService userService;
    private final SubjectManager subjectManager;

    public CibaAuthenticationRequestResolver(Domain domain, JWSService jwsService, JWKService jwkService, UserService userService, SubjectManager subjectManager) {
        this.domain = domain;
        this.jwsService = jwsService;
        this.jwkService = jwkService;
        this.userService = userService;
        this.subjectManager = subjectManager;
    }

    public Single resolve(CibaAuthenticationRequest authRequest, Client client) {
        return resolveAuthorizedScopes(authRequest, client, null).flatMap(req -> {
            if (!StringUtils.isEmpty(req.getIdTokenHint())) {
                return parseJwt(req.getIdTokenHint()).flatMap(jwt -> {
                    if (jwt instanceof SignedJWT) {
                        return validateIdTokenHint(authRequest, (SignedJWT)jwt);
                    } else {
                        return Single.error(new InvalidRequestException("id_token_hint must be signed"));
                    }
                });
            } else if (!StringUtils.isEmpty(req.getLoginHintToken())) {
                return parseJwt(req.getLoginHintToken()).flatMap(jwt -> {
                    // Specification doesn't specify if this token have to be signed
                    if (jwt instanceof SignedJWT) {
                        return verifyLoginHintTokenSignature((SignedJWT)jwt, client)
                                .flatMap(verifiedJwt -> validateLoginHintToken(authRequest, verifiedJwt));
                    } else {
                        return validateLoginHintToken(authRequest, jwt);
                    }
                });
            } else {
                // login_hint is provided (look for username or email)
                final FilterCriteria criteria = new FilterCriteria();

                criteria.setQuoteFilterValue(true);
                criteria.setFilterName(authRequest.getLoginHint().contains("@") ? "email" : "username");
                criteria.setFilterValue(authRequest.getLoginHint());
                criteria.setOperator("eq");

                return userService.findByDomainAndCriteria(domain.getId(), criteria).map(users -> {
                    if (users.size() != 1) {
                        LOGGER.warn("login_hint match multiple users or no one");
                        throw new InvalidRequestException("Invalid hint");
                    }
                    authRequest.setSubject(users.get(0).getId());
                    return authRequest;
                });

            }
        });
    }

    private Single parseJwt(String hint) {
        return Single.fromCallable(() -> {
            try {
                return JWTParser.parse(hint);
            } catch (ParseException e) {
                throw new InvalidRequestException("hint is not a valid JWT");
            }
        });
    }

    private Single validateIdTokenHint(CibaAuthenticationRequest authRequest, SignedJWT signedJwt) {
        // id_token_hint is the id_token generate by the OP for the client, so we use the JWKS provided by the domain.
        return jwkService.getKeys()
                .flatMapMaybe(jwks -> jwkService.getKey(jwks, signedJwt.getHeader().getKeyID()))
                .switchIfEmpty(Single.error(() -> new InvalidRequestException("JWK not found for id_token_hint")))
                .filter(jwk -> jwsService.isValidSignature(signedJwt, jwk))
                .switchIfEmpty(Single.error(() -> new InvalidRequestException("Invalid signature fo id_token_hint")))
                .flatMap(jwk -> {
                    try {
                        final Date expirationTime = signedJwt.getJWTClaimsSet().getExpirationTime();
                        if (expirationTime != null) {
                            evaluateExp(expirationTime.toInstant().getEpochSecond(), Instant.now(), 0);
                        }
                        return subjectManager.findUserBySub(new io.gravitee.am.common.jwt.JWT(signedJwt.getJWTClaimsSet().getClaims())).map(user -> {
                            authRequest.setSubject(user.getId());
                            return authRequest;
                        }).toSingle();
                    } catch (ExpiredJWTException e) {
                        return Single.error(new InvalidRequestException("id_token_hint expired"));
                    }
                });
    }

    private Single verifyLoginHintTokenSignature(SignedJWT signedJwt, Client client) {
        // As the login_hint_token is provided by the client, we use the Client JWKS to validate the signature.
        // contrary to the id_token_hint that is the id_token generated by the OP for the client.
        return jwkService.getKeys(client)
                .flatMap(jwks -> jwkService.getKey(jwks, signedJwt.getHeader().getKeyID()))
                .switchIfEmpty(Single.error(() -> new InvalidRequestException("JWK not found for login_token_hint")))
                .filter(jwk -> jwsService.isValidSignature(signedJwt, jwk))
                .switchIfEmpty(Single.error(() -> new InvalidRequestException("Invalid signature fo login_token_hint")))
                .flatMap(__ -> Single.just(signedJwt));
    }

    private Single validateLoginHintToken(CibaAuthenticationRequest authRequest, JWT jwt) {
        try {
            final Date expirationTime = jwt.getJWTClaimsSet().getExpirationTime();
            if (expirationTime != null) {
                evaluateExp(expirationTime.toInstant().getEpochSecond(), Instant.now(), 0);
            }

            final JSONObject subIdObject = new JSONObject(jwt.getJWTClaimsSet().getJSONObjectClaim("sub_id"));
            /*
                sub_id is an object specifying the field identifying the user (through format entry)
                Supported format : email and username
                {
                  "sub_id": {
                    "format": "email",
                    "email": "[email protected]"
                  }
                }
             */
            final FilterCriteria criteria = new FilterCriteria();
            criteria.setQuoteFilterValue(false);
            final String field = subIdObject.getAsString("format");
            if (!"email".equals(field) && !"username".equals(field)) {
                return Single.error(new InvalidRequestException("Invalid hint, only email and username are supported"));
            }
            criteria.setFilterName(field);
            criteria.setFilterValue(subIdObject.getAsString(field));

            return userService.findByDomainAndCriteria(domain.getId(), criteria).flatMap(users -> {
                if (users.size() != 1) {
                    LOGGER.warn("login_hint_token match multiple users or no one");
                    return Single.error(new InvalidRequestException("Invalid hint"));
                }
                authRequest.setSubject(users.get(0).getId());
                return Single.just(authRequest);
            });

        } catch (ExpiredJWTException e) {
            return Single.error(new ExpiredLoginHintTokenException("login_token_hint expired"));
        } catch (ParseException e) {
            // should never happen
            LOGGER.warn("login_hint_token can't be read", e);
            return Single.error(new ExpiredLoginHintTokenException("invalid login_token_hint"));
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy