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

io.gravitee.am.gateway.handler.ciba.service.AuthenticationRequestServiceImpl 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;

import io.gravitee.am.authdevice.notifier.api.AuthenticationDeviceNotifierProvider;
import io.gravitee.am.authdevice.notifier.api.model.ADCallbackContext;
import io.gravitee.am.authdevice.notifier.api.model.ADNotificationRequest;
import io.gravitee.am.authdevice.notifier.api.model.ADNotificationResponse;
import io.gravitee.am.authdevice.notifier.api.model.ADUserResponse;
import io.gravitee.am.common.exception.oauth2.InvalidRequestException;
import io.gravitee.am.common.jwt.JWT;
import io.gravitee.am.gateway.handler.ciba.exception.AuthenticationRequestExpiredException;
import io.gravitee.am.gateway.handler.ciba.exception.AuthenticationRequestNotFoundException;
import io.gravitee.am.gateway.handler.ciba.exception.AuthorizationPendingException;
import io.gravitee.am.gateway.handler.ciba.exception.SlowDownException;
import io.gravitee.am.gateway.handler.ciba.service.request.AuthenticationRequestStatus;
import io.gravitee.am.gateway.handler.ciba.service.request.CibaAuthenticationRequest;
import io.gravitee.am.gateway.handler.common.client.ClientSyncService;
import io.gravitee.am.gateway.handler.common.jwt.JWTService;
import io.gravitee.am.gateway.handler.manager.authdevice.notifier.AuthenticationDeviceNotifierManager;
import io.gravitee.am.gateway.handler.oauth2.exception.AccessDeniedException;
import io.gravitee.am.gateway.handler.oauth2.exception.InvalidClientException;
import io.gravitee.am.model.Domain;
import io.gravitee.am.model.oidc.Client;
import io.gravitee.am.repository.oidc.api.CibaAuthRequestRepository;
import io.gravitee.am.repository.oidc.model.CibaAuthRequest;
import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Single;
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.core.env.Environment;

import java.time.Instant;
import java.util.Date;
import java.util.Optional;

import static io.gravitee.am.gateway.handler.common.jwt.JWTService.TokenType.STATE;

/**
 * @author Eric LELEU (eric.leleu at graviteesource.com)
 * @author GraviteeSource Team
 */
public class AuthenticationRequestServiceImpl implements AuthenticationRequestService {

    private static final Logger LOGGER = LoggerFactory.getLogger(AuthenticationRequestServiceImpl.class);

    @Autowired
    private CibaAuthRequestRepository authRequestRepository;

    @Autowired
    private Environment environment;

    @Autowired
    private Domain domain;

    @Autowired
    private AuthenticationDeviceNotifierManager notifierManager;

    @Autowired
    private JWTService jwtService;

    @Autowired
    private ClientSyncService clientService;

    /**
     * How many time (in sec) an auth-request is kept into the DB
     * once it expired. (This retention is useful to manage the
     * expired_token error)
     */
    @Value("${openid.ciba.auth-request.retention:900}")
    private int requestRetentionInSec = 900;

    @Override
    public Single register(CibaAuthenticationRequest request, Client client) {
        Instant now = Instant.now();
        final Integer requestedExpiry = request.getRequestedExpiry();
        final long ttl = requestedExpiry != null ? requestedExpiry: domain.getOidc().getCibaSettings().getAuthReqExpiry();

        CibaAuthRequest entity = new CibaAuthRequest();
        entity.setClientId(client.getClientId());
        entity.setId(request.getId());
        entity.setScopes(request.getScopes());
        entity.setSubject(request.getSubject());
        entity.setStatus(AuthenticationRequestStatus.ONGOING.name());
        entity.setCreatedAt(new Date(now.toEpochMilli()));
        entity.setLastAccessAt(new Date(now.toEpochMilli()));
        // as the application has to be informed of an expired request, we add retention time to the ttl
        // to avoid removing the request information from the database when ttl has expired
        entity.setExpireAt(new Date(now.plusSeconds(ttl + requestRetentionInSec).toEpochMilli()));

        LOGGER.debug("Register AuthenticationRequest with auth_req_id '{}' and expiry of '{}' seconds", entity.getId(), ttl);

        return authRequestRepository.create(entity);
    }

    @Override
    public Single retrieve(Domain domain, String authReqId) {
        LOGGER.debug("Search for authentication request with id '{}'", authReqId);
        return this.authRequestRepository.findById(authReqId)
                .switchIfEmpty(Single.error(() -> new AuthenticationRequestNotFoundException(authReqId)))
                .flatMap(request -> {
                    if ((request.getExpireAt().getTime() - (requestRetentionInSec * 1000)) < Instant.now().toEpochMilli()) {
                        return Single.error(new AuthenticationRequestExpiredException());
                    }
                    switch (AuthenticationRequestStatus.valueOf(request.getStatus())) {
                        case ONGOING:
                            // Check if the request interval is respected by the client
                            // if the client request to often the endpoint, throws a SlowDown error
                            // otherwise, update the last Access date before sending the pending exception
                            final int interval = domain.getOidc().getCibaSettings().getTokenReqInterval();
                            if (request.getLastAccessAt().toInstant().plusSeconds(interval).isAfter(Instant.now())) {
                                return Single.error(new SlowDownException());
                            }
                            request.setLastAccessAt(new Date());
                            return this.authRequestRepository.update(request).flatMap(__ -> Single.error(new AuthorizationPendingException()));
                        case REJECTED:
                            return this.authRequestRepository.delete(authReqId).toSingle(() -> { throw new AccessDeniedException(); });
                        default:
                            return this.authRequestRepository.delete(authReqId).toSingle(() -> request);
                    }
                });
    }

    @Override
    public Single updateAuthDeviceInformation(CibaAuthRequest request) {
        LOGGER.debug("Update authentication request '{}' with AuthenticationDeviceNotifier information", request.getId());
        return this.authRequestRepository.findById(request.getId())
                .switchIfEmpty(Single.error(() -> new AuthenticationRequestNotFoundException(request.getId())))
                .flatMap(existingReq -> {
                    // update only information provided by the AD notifier
                    existingReq.setExternalTrxId(request.getExternalTrxId());
                    existingReq.setExternalInformation(request.getExternalInformation());
                    existingReq.setDeviceNotifierId(request.getDeviceNotifierId());
            return this.authRequestRepository.update(existingReq);
        });
    }

    @Override
    public Single notify(ADNotificationRequest adRequest) {
        final AuthenticationDeviceNotifierProvider notifier = this.notifierManager.getAuthDeviceNotifierProvider(adRequest.getDeviceNotifierId());

        if (notifier == null) {
            return Single.error(new InvalidRequestException("No authentication device notifier defined"));
        }

        return notifier.notify(adRequest);
    }

    @Override
    public Completable validateUserResponse(ADCallbackContext context) {
        return Flowable.fromIterable(this.notifierManager.getAuthDeviceNotifierProviders())
                .flatMapSingle(provider -> provider.extractUserResponse(context))
                .filter(Optional::isPresent)
                .firstOrError()
                .map(Optional::get)
                .flatMap(userResponse -> {
                    final String status = userResponse.isValidated() ? AuthenticationRequestStatus.SUCCESS.name() : AuthenticationRequestStatus.REJECTED.name();
                    return this.jwtService.decode(userResponse.getState(), STATE)
                            .flatMap(jwtState -> verifyState(userResponse, jwtState.getAud())
                            ).flatMap(verifiedJwtState -> updateRequestStatus(verifiedJwtState.getJti(), status));
                }).ignoreElement();
    }

    private Single verifyState(ADUserResponse userResponse, String clientId) {
        LOGGER.debug("Prepare verification of state '{}' with client id '{}'", userResponse.getState(), clientId);
        return this.clientService.findByClientId(clientId)
                .switchIfEmpty(Single.error(InvalidClientException::new))
                .flatMap(client -> Single.defer(() -> this.jwtService.decodeAndVerify(userResponse.getState(), client, STATE)))
                .filter(verifiedJwt -> userResponse.getTid().equals(verifiedJwt.getJti()))
                .switchIfEmpty(Single.error(() -> new InvalidRequestException("state parameter mismatch with the transaction id")))
                .onErrorResumeNext((error) -> {
                    LOGGER.debug("Verification of state '{}' fails on CIBA callback with client id '{}'", userResponse.getState(), clientId, error);
                    return Single.error(new InvalidRequestException("Invalid CIBA State"));
                });
    }

    private Single updateRequestStatus(String reqExtId, String status) {
        return this.authRequestRepository.findByExternalId(reqExtId)
                .switchIfEmpty(Single.error(() -> new InvalidRequestException("Invalid CIBA State")))
                .flatMap(cibaRequest -> this.authRequestRepository.updateStatus(cibaRequest.getId(), status));
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy