Skip to content

JDBC implementation of RegisteredClientRepository #291

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed

Conversation

rlewczuk
Copy link
Contributor

@rlewczuk rlewczuk commented May 12, 2021

Code seems to be more-or-less ready for preliminary review, yet I still have some questions in comments below.

Closes gh-265

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label May 12, 2021
@rlewczuk
Copy link
Contributor Author

rlewczuk commented May 12, 2021

Question: provided set of tests mostly copies those from InMemoryRegisteredClientRepositoryTests. This leads to considerable amount of code duplication, grouping those tests into common superclass would not only reduce duplication but also create baseline for testing future implementations of RegisteredClientRepository (eg. Redis variant). For now I've decided against it, so I don't touch any unrelated code. Should I refactor those into common base ? If so, should it be done as part of this change or as separate one ?

@rlewczuk rlewczuk force-pushed the gh-265-jdbc-client-repository branch 2 times, most recently from 597416c to ee9f2f5 Compare May 12, 2021 05:06
@rlewczuk
Copy link
Contributor Author

rlewczuk commented May 12, 2021

Question: as RegisteredClient is immutable, there us no update semantics implemented in JdbcRegisteredClientRepository. Is it valid approach ?

@rlewczuk
Copy link
Contributor Author

Question: JdbcClientRepository checks for duplicated client secret. Does it make sens when password encoders are used ?

@jgrandja jgrandja changed the title JDBC implementation of RegisteredClientRepository closes #265 JDBC implementation of RegisteredClientRepository May 12, 2021
@rlewczuk rlewczuk force-pushed the gh-265-jdbc-client-repository branch from ee9f2f5 to 5842990 Compare May 12, 2021 15:52
@lanmingle
Copy link

你好!

我曾经在使用 mongo 进行适配,我建议使用转换器,这样更灵活。


Hello!

I used to use mongo for adaptation, and I recommend using a converter, which is more flexible.


Reference Code

model

@Getter
@Setter
@ToString
@EqualsAndHashCode(of = {"key"})
public class OidcPersistentKeyValue {

    private String key;

    private Object value;

    public OidcPersistentKeyValue() {
    }

    public OidcPersistentKeyValue(String key, Object value) {
        this.key = key;
        this.value = value;
    }

}

/**
 * @see org.springframework.security.oauth2.server.authorization.config.ClientSettings
 */
@Getter
@Setter
@ToString(callSuper = true)
public class OidcClientSettings extends OidcPersistentKeyValue {

    public static final String CLIENT_SETTING_BASE = "setting.client.";

    public static final String REQUIRE_PROOF_KEY = CLIENT_SETTING_BASE.concat("require-proof-key");

    public static final String REQUIRE_USER_CONSENT = CLIENT_SETTING_BASE.concat("require-user-consent");

    public OidcClientSettings() {
    }

    public OidcClientSettings(String key, Object value) {
        super(key, value);
    }

}

/**
 * @see org.springframework.security.oauth2.server.authorization.config.TokenSettings
 */
@Getter
@Setter
@ToString(callSuper = true)
public class OidcTokenSettings extends OidcPersistentKeyValue {

    public static final String TOKEN_SETTING_BASE = "setting.token.";

    public static final String ACCESS_TOKEN_TIME_TO_LIVE = TOKEN_SETTING_BASE.concat("access-token-time-to-live");

    public static final String REUSE_REFRESH_TOKENS = TOKEN_SETTING_BASE.concat("reuse-refresh-tokens");

    public static final String REFRESH_TOKEN_TIME_TO_LIVE = TOKEN_SETTING_BASE.concat("refresh-token-time-to-live");

    public OidcTokenSettings() {
    }

    public OidcTokenSettings(String key, Object value) {
        super(key, value);
    }

}

/**
 * @see org.springframework.security.oauth2.core.ClientAuthenticationMethod
 */
public enum OidcClientAuthenticationMethod {

    BASIC,

    POST,

    NONE,

}

/**
 * @see org.springframework.security.oauth2.core.AuthorizationGrantType
 */
public enum OidcAuthorizationGrantType {

    AUTHORIZATION_CODE,

//    @Deprecated
//    IMPLICIT,

    REFRESH_TOKEN,

    CLIENT_CREDENTIALS,

    PASSWORD,

}

domain

@Getter
@Setter
@ToString
@EqualsAndHashCode(of = {"id"})
@Document(collection = "OidcRegisteredClient")
@CompoundIndexes({
        @CompoundIndex(def = "{'clientId': 1}", unique = true)
})
public class OidcRegisteredClient {

    @Id
    private String id;

    private String clientId;

    private String clientSecret;

    private Set<OidcClientAuthenticationMethod> clientAuthenticationMethods;

    private Set<OidcAuthorizationGrantType> authorizationGrantTypes;

    private Set<String> redirectUris;

    private Set<String> scopes;

    private Set<OidcClientSettings> clientSettings;

    private Set<OidcTokenSettings> tokenSettings;

}

repository

public interface OidcRegisteredClientRepository extends MongoRepository<OidcRegisteredClient, String> {

    @Query("{ 'clientId': :#{#clientId} }")
    Optional<OidcRegisteredClient> getByClientId(@Param("clientId") String clientId);

}

converter

@Slf4j
@Component
public class RegisteredClientConverter implements Converter<OidcRegisteredClient, RegisteredClient> {

    @Override
    public RegisteredClient convert(OidcRegisteredClient source) {
        return this.toConvert(OidcRegisteredClientBuilder.create(source).build());
    }

    private RegisteredClient toConvert(OidcRegisteredClient merge) {
        return RegisteredClient.withId(merge.getId())
                .clientId(merge.getClientId())
                .clientSecret(merge.getClientSecret())
                .clientAuthenticationMethods((consumer) -> this.toClientAuthenticationMethods(consumer, merge.getClientAuthenticationMethods()))
                .authorizationGrantTypes((consumer) -> this.toAuthorizationGrantTypes(consumer, merge.getAuthorizationGrantTypes()))
                .redirectUris((consumer) -> this.toRedirectUris(consumer, merge.getRedirectUris()))
                .scopes((consumer) -> this.toScopes(consumer, merge.getScopes()))
                .clientSettings((consumer) -> this.toClientSettings(consumer, merge.getClientSettings()))
                .tokenSettings((consumer) -> this.toTokenSettings(consumer, merge.getTokenSettings()))
                .build();
    }

    private void toClientAuthenticationMethods(Set<ClientAuthenticationMethod> sources, Set<OidcClientAuthenticationMethod> others) {
        if (CollUtil.isNotEmpty(others)) {
            others.forEach(source -> sources.add(new ClientAuthenticationMethod(source.name().toLowerCase())));
        }
    }

    private void toAuthorizationGrantTypes(Set<AuthorizationGrantType> sources, Set<OidcAuthorizationGrantType> others) {
        if (CollUtil.isNotEmpty(others)) {
            others.forEach(source -> sources.add(new AuthorizationGrantType(source.name().toLowerCase())));
        }
    }

    private void toRedirectUris(Set<String> sources, Set<String> others) {
        if (CollUtil.isNotEmpty(others)) {
            sources.addAll(others.stream().filter(StrUtil::isNotEmpty).collect(Collectors.toSet()));
        }
    }

    private void toScopes(Set<String> sources, Set<String> others) {
        if (CollUtil.isNotEmpty(others)) {
            sources.addAll(others.stream().filter(StrUtil::isNotEmpty).collect(Collectors.toSet()));
        }
    }

    private void toClientSettings(ClientSettings source, Set<OidcClientSettings> others) {
        if (CollUtil.isNotEmpty(others)) {
            others.forEach(other -> {
                String key = other.getKey();
                Object val = other.getValue();
                if (ClientSettings.REQUIRE_PROOF_KEY.equals(key)) {
                    source.setting(key, Convert.convert(Boolean.class, val, OidcRegisteredClientBuilder.REQUIRE_PROOF_KEY));
                } else if (ClientSettings.REQUIRE_USER_CONSENT.equals(key)) {
                    source.setting(key, Convert.convert(Boolean.class, val, OidcRegisteredClientBuilder.REQUIRE_USER_CONSENT));
                }
            });
        }
    }

    private void toTokenSettings(TokenSettings source, Set<OidcTokenSettings> others) {
        if (CollUtil.isNotEmpty(others)) {
            others.forEach(other -> {
                String key = other.getKey();
                Object val = other.getValue();
                if (TokenSettings.ACCESS_TOKEN_TIME_TO_LIVE.equals(key)) {
                    source.setting(key, Convert.convert(Duration.class, val, OidcRegisteredClientBuilder.ACCESS_TOKEN_TIME_TO_LIVE));
                } else if (TokenSettings.REFRESH_TOKEN_TIME_TO_LIVE.equals(key)) {
                    source.setting(key, Convert.convert(Duration.class, val, OidcRegisteredClientBuilder.REFRESH_TOKEN_TIME_TO_LIVE));
                } else if (TokenSettings.REUSE_REFRESH_TOKENS.equals(key)) {
                    source.setting(key, Convert.convert(Boolean.class, val, OidcRegisteredClientBuilder.REUSE_REFRESH_TOKENS));
                }
            });
        }
    }

}

RegisteredClientRepository

@Slf4j
@Repository
public class DelegateRegisteredClientRepository implements RegisteredClientRepository {

    @Autowired
    private OidcRegisteredClientService registeredClientService;

    @Autowired
    private RegisteredClientConverter registeredClientConverter;

    @Override
    public RegisteredClient findById(String id) {
        return this.registeredClientService.getById(id).map(registeredClient -> this.registeredClientConverter.convert(registeredClient)).orElse(null);
    }

    @Override
    public RegisteredClient findByClientId(String clientId) {
        return this.registeredClientService.getByClientId(clientId).map(registeredClient -> this.registeredClientConverter.convert(registeredClient)).orElse(null);
    }

}

@jgrandja jgrandja self-assigned this May 20, 2021
@jgrandja jgrandja added type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged labels May 20, 2021
@jgrandja jgrandja added this to the 0.1.2 milestone May 20, 2021
@jgrandja jgrandja assigned sjohnr and unassigned jgrandja Jun 8, 2021
@jgrandja
Copy link
Collaborator

jgrandja commented Jun 8, 2021

@rlewczuk

FYI, @sjohnr is a new member on our team and he will be handling the review of this PR. We are still targeting this for the 0.1.2 release (Jul 8) so @sjohnr will be providing initial feedback soon.

Copy link
Contributor

@sjohnr sjohnr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @rlewczuk, thanks for your work on this! Just so you know, I'm about to head out of town for the week, but wanted to give a bit of feedback sooner than later. This feedback focuses on the schema and two additional items. I'll do a more thorough review of the code early next week hopefully.

See feedback inline.

require_user_consent boolean not null,
access_token_ttl integer default 300000 not null,
reuse_refresh_tokens boolean default true not null,
refresh_token_ttl integer default 600000 not null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the settings, please consider using a JSON field, client_settings varchar(1000) DEFAULT NULL and token_settings varchar(1000) DEFAULT NULL. See #304 for an example of this (e.g. attributes).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented but I have the same remarks as with client_secret LOB. It is more complex both in code and in day-to-day maintenance if application administrator wants to make manual changes in database.

Copy link
Contributor

@sjohnr sjohnr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rlewczuk I've added some additional comments and requested a few more changes. Let me know if you have questions or comments. Please see my feedback inline.

@rlewczuk
Copy link
Contributor Author

I'll take care of it next week or so as I'm currently bogged with other project I'm finishing right now. Thanks for review.

@rlewczuk rlewczuk force-pushed the gh-265-jdbc-client-repository branch from e15bb58 to 889ca01 Compare June 22, 2021 06:24
@rlewczuk
Copy link
Contributor Author

Ok, all fixes are here and we're ready for second round.

sjohnr pushed a commit that referenced this pull request Jun 22, 2021
@sjohnr
Copy link
Contributor

sjohnr commented Jun 22, 2021

Thanks @rlewczuk! This is now in main as 769cf8f.

@sjohnr sjohnr closed this Jun 22, 2021
@sjohnr sjohnr added the status: duplicate A duplicate of another issue label Jun 28, 2021
sjohnr pushed a commit that referenced this pull request Jun 29, 2021
jgrandja added a commit that referenced this pull request Jul 9, 2021
@rlewczuk rlewczuk deleted the gh-265-jdbc-client-repository branch November 14, 2021 21:27
doba16 pushed a commit to doba16/spring-authorization-server that referenced this pull request Apr 21, 2023
doba16 pushed a commit to doba16/spring-authorization-server that referenced this pull request Apr 21, 2023
doba16 pushed a commit to doba16/spring-authorization-server that referenced this pull request Apr 21, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: duplicate A duplicate of another issue type: enhancement A general enhancement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Provide JDBC implementation of RegisteredClientRepository
5 participants