feat: Configurable OAuth2 login page
Address the issue: https://github.com/spring-cloud/spring-cloud-dataflow-ui/issues/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: https://github.com/spring-cloud/spring-cloud-dataflow-ui/pull/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>
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.
I also actually don't know why this test is failing 😨
Could it be that I changed the AboutController and the ClientRegistrations are provided in the SecurityInfo response, now?
Hi @klopfdreh , I will try to take a look sometime today.
Thanks a lot!
Hey @onobc - I think I found a solution - see: https://github.com/klopfdreh/spring-cloud-dataflow/pull/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:
- I entered the url
http://localhost:9393/dashboard - The new dashboard login page with the registrations is shown
- When I click on a registration I am redirected to the OAuth server's consent page
- When I allowed the scopes to the backend I can see the dashboard
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!
@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.
No worries - I am glad to help a bit in my free time.
@onobc - Build is repaired 👍 - I am adjust the skipper to work the same way in SkipperOAuthSecurityConfiguration.
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.
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.
@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.
All right! If there are any further questions just ping me. 👍
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. 😄
Resolved the conflicts to integrate the updates for the AboutController. Other than that the UI looks good to me. thank you @klopfdreh
@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.
@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: https://github.com/spring-cloud/spring-cloud-dataflow/pull/5350#discussion_r1209886862
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 :)
Awesome! 😄