001/*-
002 * #%L
003 * Smile CDR - CDR
004 * %%
005 * Copyright (C) 2016 - 2026 Smile CDR, Inc.
006 * %%
007 * All rights reserved.
008 * #L%
009 */
010package ca.cdr.test.app.clients;
011
012import com.fasterxml.jackson.databind.JsonNode;
013import jakarta.annotation.Nonnull;
014import jakarta.annotation.Nullable;
015
016import java.util.Objects;
017
018/**
019 * A successful OAuth 2.0 token endpoint response.
020 *
021 * @param accessToken  the {@code access_token}
022 * @param tokenType    the {@code token_type}, normally {@code Bearer}
023 * @param expiresIn    the {@code expires_in} lifetime in seconds, if the server sent one
024 * @param scope        the granted {@code scope}, if the server sent one
025 * @param idToken      the OpenID Connect {@code id_token}, if one was issued
026 * @param refreshToken the {@code refresh_token}, if one was issued
027 * @param raw          the whole response, for any member not listed above
028 * @see <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-5.1">RFC 6749 section 5.1</a>
029 */
030// Created by Claude Opus 5.5
031public record AccessTokenResponse(
032                @Nonnull String accessToken,
033                @Nullable String tokenType,
034                @Nullable Long expiresIn,
035                @Nullable String scope,
036                @Nullable String idToken,
037                @Nullable String refreshToken,
038                @Nonnull JsonNode raw) {
039
040        /**
041         * Reads a token endpoint response body.
042         *
043         * @param theResponse the parsed response body
044         * @return the response
045         * @throws IllegalArgumentException if the body carries no {@code access_token}
046         */
047        public static @Nonnull AccessTokenResponse fromJson(@Nonnull JsonNode theResponse) {
048                Objects.requireNonNull(theResponse, "theResponse must not be null");
049                String accessToken = textOrNull(theResponse, "access_token");
050                if (accessToken == null) {
051                        throw new IllegalArgumentException("access_token not found in response: " + theResponse);
052                }
053                JsonNode expiresIn = theResponse.get("expires_in");
054                return new AccessTokenResponse(
055                                accessToken,
056                                textOrNull(theResponse, "token_type"),
057                                expiresIn == null || expiresIn.isNull() ? null : expiresIn.asLong(),
058                                textOrNull(theResponse, "scope"),
059                                textOrNull(theResponse, "id_token"),
060                                textOrNull(theResponse, "refresh_token"),
061                                theResponse);
062        }
063
064        private static @Nullable String textOrNull(JsonNode theNode, String theField) {
065                JsonNode value = theNode.get(theField);
066                return value == null || value.isNull() ? null : value.asText();
067        }
068}