Skip to content
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

feat: Configurable OAuth2 login page #5350

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

klopfdreh
Copy link
Contributor

@klopfdreh klopfdreh commented May 19, 2023

Address the issue: spring-cloud/spring-cloud-dataflow-ui#1887

When OAuth2 is enabled and there are client-registrations with authorization-grand-type authorization_code all those are going to be listed within the Angular-Login, now. Also when you logout and login again you are redirected correctly.

You can see the appearance here: spring-cloud/spring-cloud-dataflow-ui#1923

❗ There is one issue left I couldn't solve yet. When I fill the getAuthenticatedPaths() the loginUrl can be accessed because of to many redirects - even if this.authorizationProperties.getPermitAllPaths().add(authorizationProperties.getLoginUrl()); is added. I am going to mark this as a comment.

To test OAuth2 locally I used the following configuration (important properties are login-url: "/dashboard/#/authentication-required" and the client registration user_login):

spring:
  security:
    oauth2:
      resourceserver:
        opaquetoken:
          introspection-uri: http://localhost:8080/api/introspect
          client-id: 'vsrc13725ClientId'
          client-secret: 'vsrc13725ClientId'
      client:
        registration:
          user_login:
            provider: user_login
            client-id: 'vsrc13725ClientId'
            client-secret: 'vsrc13725Secret'
            client-authentication-method: client_secret_basic
            redirect-uri: http://localhost:9393/login/oauth2/code/user_login
            authorization-grant-type: authorization_code
            scope:
              - read
              - write
              - admin
          program_login:
            provider: program_login
            client-id: 'vsrc13725ClientId'
            client-secret: 'vsrc13725Secret'
            authorization-grant-type: client_credentials
            scope:
              - read
              - write
              - admin
        provider:
          user_login:
            token-uri: http://localhost:8080/oauth/token
            user-info-uri: http://localhost:8080/api/users/me
            user-name-attribute: email
            authorization-uri: http://localhost:8080/oauth/authorize
          program_login:
            token-uri: http://localhost:8080/oauth/token
  cloud:
    dataflow:
      security:
        authorization:
          defaultProviderId: program_login
          provider-role-mappings:
            user_login:
              map-oauth-scopes: true
              parse-oauth-scope-path-parts: false
              role-mappings:
                ROLE_CREATE: 'write'
                ROLE_DEPLOY: 'write'
                ROLE_DESTROY: 'write'
                ROLE_MANAGE: 'admin'
                ROLE_MODIFY: 'write'
                ROLE_SCHEDULE: 'write'
                ROLE_VIEW: 'read'
            program_login:
              map-oauth-scopes: true
              parse-oauth-scope-path-parts: false
              role-mappings:
                ROLE_CREATE: 'write'
                ROLE_DEPLOY: 'write'
                ROLE_DESTROY: 'write'
                ROLE_MANAGE: 'admin'
                ROLE_MODIFY: 'write'
                ROLE_SCHEDULE: 'write'
                ROLE_VIEW: 'read'
          login-url: "/dashboard/#/authentication-required"
          permit-all-paths: "/management/health,/management/health/liveness,/management/health/readiness,/management/info,/security/info,/assets/**,/dashboard/logout-success-oauth.html,/favicon.ico"
          rules:
            # About

            - GET    /about                          => hasRole('ROLE_VIEW')

            # Audit

            - GET /audit-records                     => hasRole('ROLE_VIEW')
            - GET /audit-records/**                  => hasRole('ROLE_VIEW')

            # Boot Endpoints

            - GET /management/**                  => hasRole('ROLE_MANAGE')

            # Apps

            - GET    /apps                           => hasRole('ROLE_VIEW')
            - GET    /apps/**                        => hasRole('ROLE_VIEW')
            - DELETE /apps/**                        => hasRole('ROLE_DESTROY')
            - POST   /apps                           => hasRole('ROLE_CREATE')
            - POST   /apps/**                        => hasRole('ROLE_CREATE')
            - PUT    /apps/**                        => hasRole('ROLE_MODIFY')

            # Completions

            - GET /completions/**                    => hasRole('ROLE_VIEW')

            # Job Executions & Batch Job Execution Steps && Job Step Execution Progress

            - GET    /jobs/executions                => hasRole('ROLE_VIEW')
            - PUT    /jobs/executions/**             => hasRole('ROLE_MODIFY')
            - GET    /jobs/executions/**             => hasRole('ROLE_VIEW')
            - GET    /jobs/thinexecutions            => hasRole('ROLE_VIEW')

            # Batch Job Instances

            - GET    /jobs/instances                 => hasRole('ROLE_VIEW')
            - GET    /jobs/instances/*               => hasRole('ROLE_VIEW')

            # Running Applications

            - GET    /runtime/streams                => hasRole('ROLE_VIEW')
            - GET    /runtime/streams/**             => hasRole('ROLE_VIEW')
            - GET    /runtime/apps                   => hasRole('ROLE_VIEW')
            - GET    /runtime/apps/**                => hasRole('ROLE_VIEW')

            # Stream Definitions

            - GET    /streams/definitions            => hasRole('ROLE_VIEW')
            - GET    /streams/definitions/*          => hasRole('ROLE_VIEW')
            - GET    /streams/definitions/*/related  => hasRole('ROLE_VIEW')
            - GET    /streams/definitions/*/applications  => hasRole('ROLE_VIEW')
            - POST   /streams/definitions            => hasRole('ROLE_CREATE')
            - DELETE /streams/definitions/*          => hasRole('ROLE_DESTROY')
            - DELETE /streams/definitions            => hasRole('ROLE_DESTROY')

            # Stream Deployments

            - DELETE /streams/deployments/*          => hasRole('ROLE_DEPLOY')
            - DELETE /streams/deployments            => hasRole('ROLE_DEPLOY')
            - POST   /streams/deployments/**         => hasRole('ROLE_MODIFY')
            - GET    /streams/deployments/**         => hasRole('ROLE_VIEW')

            # Stream Validations

            - GET /streams/validation/               => hasRole('ROLE_VIEW')
            - GET /streams/validation/*              => hasRole('ROLE_VIEW')

            # Stream Logs
            - GET /streams/logs/**                    => hasRole('ROLE_VIEW')

            # Task Definitions

            - POST   /tasks/definitions              => hasRole('ROLE_CREATE')
            - DELETE /tasks/definitions/*            => hasRole('ROLE_DESTROY')
            - DELETE /tasks/definitions              => hasRole('ROLE_DESTROY')
            - GET    /tasks/definitions              => hasRole('ROLE_VIEW')
            - GET    /tasks/definitions/*            => hasRole('ROLE_VIEW')

            # Task Executions

            - GET    /tasks/executions               => hasRole('ROLE_VIEW')
            - GET    /tasks/executions/*             => hasRole('ROLE_VIEW')
            - POST   /tasks/executions               => hasRole('ROLE_DEPLOY')
            - POST   /tasks/executions/*             => hasRole('ROLE_DEPLOY')
            - DELETE /tasks/executions/*             => hasRole('ROLE_DESTROY')
            - DELETE /tasks/executions               => hasRole('ROLE_DESTROY')
            - GET    /tasks/info/*                   => hasRole('ROLE_VIEW')

            # Task Schedules

            - GET    /tasks/schedules                => hasRole('ROLE_VIEW')
            - GET    /tasks/schedules/*              => hasRole('ROLE_VIEW')
            - GET    /tasks/schedules/instances      => hasRole('ROLE_VIEW')
            - GET    /tasks/schedules/instances/*    => hasRole('ROLE_VIEW')
            - POST   /tasks/schedules                => hasRole('ROLE_SCHEDULE')
            - DELETE /tasks/schedules/*              => hasRole('ROLE_SCHEDULE')
            - DELETE /tasks/schedules                => hasRole('ROLE_SCHEDULE')

            # Task Platform Account List */

            - GET    /tasks/platforms                => hasRole('ROLE_VIEW')

            # Task Validations

            - GET    /tasks/validation/               => hasRole('ROLE_VIEW')
            - GET    /tasks/validation/*              => hasRole('ROLE_VIEW')

            # Task Logs
            - GET /tasks/logs/*                       => hasRole('ROLE_VIEW')

            # Task Ctr
            - GET    /tasks/ctr/*                     => hasRole('ROLE_VIEW')

            # Tools

            - POST   /tools/**                       => hasRole('ROLE_VIEW')

and in the server I switched to my locally build UI with:

pom.xml in spring-cloud-dataflow-server

		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-dataflow-ui</artifactId>
			<version>${project.version}</version>
			<exclusions>
				<exclusion>
					<artifactId>spring-cloud-dataflow-ui</artifactId>
					<groupId>org.springframework.cloud</groupId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<artifactId>spring-cloud-dataflow-ui</artifactId>
			<groupId>org.springframework.cloud</groupId>
			<version>3.3.4-SNAPSHOT</version>
		</dependency>

@klopfdreh
Copy link
Contributor Author

klopfdreh commented May 19, 2023

Hey @onobc - I am not that good in Spring Security configuration, but I thought when I call permitAll() first and after this authenticated() for a more concrete page pattern, that those paths are going to be whitelisted. This seems to fail in my case - can you or someone of the SCDF team help me out there?

Other than that the OAuth2 Angular login is working.

@klopfdreh
Copy link
Contributor Author

I also actually don't know why this test is failing 😨

Bildschirmfoto 2023-05-19 um 13 18 10

Could it be that I changed the AboutController and the ClientRegistrations are provided in the SecurityInfo response, now?

@onobc
Copy link
Contributor

onobc commented May 19, 2023

Hi @klopfdreh ,
I will try to take a look sometime today.

@klopfdreh
Copy link
Contributor Author

Thanks a lot!

@klopfdreh
Copy link
Contributor Author

klopfdreh commented May 20, 2023

Hey @onobc - I think I found a solution - see: klopfdreh#2

The UI already checks the security in the root path. So I adjusted the settings so that the login page is allowed.

The login-url I adjusted a bit: /dashboard/index.html#/authentication-required

with this you can now login without having an impact on other services. But please verify that I did not missed anything.

Here are some screenshots what happens:

  1. I entered the url http://localhost:9393/dashboard
Bildschirmfoto 2023-05-20 um 22 59 30 2. The new dashboard login page with the registrations is shown Bildschirmfoto 2023-05-20 um 22 59 40 3. When I click on a registration I am redirected to the OAuth server's consent page Bildschirmfoto 2023-05-20 um 22 59 49 4. When I allowed the scopes to the backend I can see the dashboard Bildschirmfoto 2023-05-20 um 23 00 05

@klopfdreh
Copy link
Contributor Author

klopfdreh commented May 20, 2023

One further good thing to mention is that you still can use the spring-security login page if you want to - I just forgot about this!

@onobc
Copy link
Contributor

onobc commented May 20, 2023

@klopfdreh thanks for the updates. I did not get a chance to get to this yesterday. I will be sure to dedicate some time early this work week to thoroughly review the proposal. Again, thank you for all you do.

@klopfdreh
Copy link
Contributor Author

No worries - I am glad to help a bit in my free time.

@klopfdreh
Copy link
Contributor Author

@onobc - Build is repaired 👍 - I am adjust the skipper to work the same way in SkipperOAuthSecurityConfiguration.

@klopfdreh
Copy link
Contributor Author

The last thing I could think about is what clientRegistrations are returned from the SecurityController - currently the registrations are filtered for those which have AuthorizationGrantType set to authorization_code as those are the important to be used for the user OAuth2 login within the UI.

The standard spring-security OAuth2 login form also filters for authorization_code.

Currently I would stick to this, but this could be improved that the all clientRegistrations are returned as complex objects and the UI filters for `authorization_code.

@klopfdreh
Copy link
Contributor Author

I tested the toggle between

login-url: "/login" (default spring-security oauth2 login)

and

login-url: "/dashboard/index.html#/authentication-required" (angular login)

both is working now.

@corneil
Copy link
Contributor

corneil commented May 24, 2023

@klopfdreh Thank you for this contribution.
We will be able to give it the attention it deserves once we have 2.11.0 out the door.

@corneil corneil added this to the Priority Backlog milestone May 24, 2023
@klopfdreh
Copy link
Contributor Author

All right! If there are any further questions just ping me. 👍


ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry security;

if (AuthorizationProperties.FRONTEND_LOGIN_URL.equals(this.authorizationProperties.getLoginUrl())) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Those security settings need to be checked before merging.

@klopfdreh
Copy link
Contributor Author

Let me know when you are able to have a look at this change. Might be cool to get a fancy Angular Login page instead of simple text. 😄

@claudiahub
Copy link

Resolved the conflicts to integrate the updates for the AboutController.
Other than that the UI looks good to me.
thank you @klopfdreh

@onobc onobc added the status/on-hold For various reasons is on hold label Nov 10, 2023
@onobc onobc marked this pull request as draft November 10, 2023 15:02
@onobc onobc marked this pull request as ready for review November 10, 2023 15:02
@onobc
Copy link
Contributor

onobc commented Nov 10, 2023

@klopfdreh we will need to wait post 2.11.2 for this to get in. We are busy fixing fallout from the big move to support Boot3. Once the dust settles we will get this in.

Thanks for your patience.

@klopfdreh
Copy link
Contributor Author

@onobc - just a short reminder - would be nice to have this implemented as we can adjust our CSP settings to not allow any content from the bootstrap CDN anymore 👍 (zero trust)

Important hint: A review needs to be done before merging: #5350 (comment)

@onobc
Copy link
Contributor

onobc commented Feb 7, 2024

Hi @klopfdreh - thanks for the reminder. Not exactly sure when we will get to this but it is on the front burner again w/ your ping :)

@klopfdreh
Copy link
Contributor Author

Awesome! 😄

@cppwfs cppwfs modified the milestones: Priority Backlog, 2.11.x Apr 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status/on-hold For various reasons is on hold
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants