Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/joshlong-attic/springone-2024-goodbye-passwords

legendary Spring Security lead Rob Winch and his trusty and faithfiul sidekick explore the wide and wonderful world of Spring Security and passwords
https://github.com/joshlong-attic/springone-2024-goodbye-passwords

Last synced: 1 day ago
JSON representation

legendary Spring Security lead Rob Winch and his trusty and faithfiul sidekick explore the wide and wonderful world of Spring Security and passwords

Awesome Lists containing this project

README

        

# Goodbye, Passwords

with Spring Security lead Rob Winch and Spring Developer Advocate Josh Long

[Slides](https://docs.google.com/presentation/d/1mi6xGsGBq9PjxzOj1W9oEtFHBnfE61X8nrr5K_FcHLo/edit?usp=sharing)

## preflight
* go to safari and delete all the old passkeys. TAN: why are passkeys for the entire OS in their bloody browser?
* shut down all the docker compose instances
* reset database with new import
* install the schema in `misc/legacy-schema.sql` to PostgreSQL by going to `/Users/jlong/Desktop/talk/springone-2024/passwordless-with-rob-winch` and running `init_db.sh`
* Bring up slides

## demo
* intro
* authN v authZ
* MAKE SURE TO MIGRATE TO `application.yml` ASAP.
* start.spring.io: service is called `auth`, add: `web`, `jdbc`, `security`, `graalvm`, and `postgresql`.
* create a simple hello controller, restart
```
@Controller
@ResponseBody
class SecuredEndpoint {

@GetMapping("/")
Map hello(Principal principal) {
return Map.of("hello", principal.getName());
}
}
```
* spring boot configures default user/random password
* where does that come from? u can change with properties
* but ultimately it's an object. the root of all authentication is `AuthenticationManager`. its a little open ended.
* `UserDetailsService` concerns itself specifically with passwords, which are a common type of authentication (but not the only ones, importantly for later)
* register `InMemoryUserDetailsManager` with different users and roles for each
```
@Bean
InMemoryUserDetailsManager userDetailsService() {
var users = User.withDefaultPasswordEncoder();
return new InMemoryUserDetailsManager(
users.username("rob").password("pw").roles("USER", "ADMIN").build(),
users.username("josh").password("pw").roles("USER").build()
);
}

```
* add the endpoint:
```
@GetMapping ("/admin")
Map helloPrincipal(Principal principal) {
return Map.of("hello administrator ", principal.getName());
}

```
* now what do those roles buy us? let's say we created another endpoint (admin-only endpoint) and want to secure it. configure authorization based on roles, like this.
```
@Bean
SecurityFilterChain securityWebFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.authorizeHttpRequests(http -> http
.requestMatchers("/admin").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.build();
}
```
* so weve got authz,n; go to the `InMemoryUserDetailsManager` and print out rob's `getPasssword()`; it's encoded! Who's done that? `PasswordEncoder` did that.
* RW: password history discussion by Rob
* make sure to disambiguate encoding vs encryption
* turns out we have an existing system setup using sha256. as we talked about, that's no longer secure. but how do we migrate them if the password encoding is a one way ticket? simple, we do it at the time of login, when we have a known-to-be-valid password.
* to see this in action, we have to fix a few things:
* we need to migrate to a durable store. let's use JDBC.
```
@Bean
JdbcUserDetailsManager jdbcUserDetailsService(DataSource dataSource) {
return new JdbcUserDetailsManager(dataSource);
}
```
* as it stands we won't be able to login. our passwords are in the old format. let's fix that.
* run `select * from users`; see the problem? the data is old.
* run a migration to set the password to be : `update users set password = '{sha256}' || password `
* and finally the JDBC store won't automatically handle persisting the updated passwords.
* RW: talk about password upgrades
```
@Bean
UserDetailsPasswordService userDetailsPasswordService(UserDetailsManager userDetailsManager) {
return (user, newPassword) -> {
var updated = User.withUserDetails(user).password(newPassword).build();
userDetailsManager.updateUser(updated);
return updated;
};
}
```
* now restart the app and login as rob or josh and see that their pw in the db users table has been upgraded transparently. nice!
* RW: Passkeys slides
* ensure to install webauthn support
```
git clone https://github.com/rwinch/spring-security-webauthn/
cd spring-security-webauthn
./gradlew publishToMavenLocal
```
* add the dependency:
```

io.github.rwinch.webauthn
spring-security-webauthn
0.0.1-SNAPSHOT

```
* Add snapshot repository
```


spring-snapshot
https://repo.spring.io/snapshot


```
* specify the following in `pom.xml`:
```
6.4.0-SNAPSHOT
```
* add the following to the config:
```
.with(webauthn() ,c -> c
.allowedOrigins("http://localhost:8080")
.rpId("localhost")
.rpName("Bootiful Passkeys")
)
```
* start the app and visit http://localhost:8080/webauthn/register
* logout
* http://localhost:8080/ log in with passkey
* NOTE: If fails check if remembered to clear old passkeys
* RW: magic links 101 - slides
* add the onetimetoken configuration element:
```
.oneTimeTokenLogin(configurer -> configurer.generatedOneTimeTokenSuccessHandler((request, response, oneTimeToken) -> {
var msg = "go to http://localhost:8080/login/ott?token=" + oneTimeToken.getTokenValue();
System.out.println(msg);
response.setContentType(MediaType.TEXT_PLAIN_VALUE);
response.getWriter().write("you've got console mail!");
}))

```
* EZ! were now logged in with naught but a magic link. now what about your services? do you need to login like this for each service u stand up? wouldnt it be nice if you could centralize all this configuration in one place and give your other services and appos a means to benefit from that?
* that's the power of Spring Authorization Service.
* to see it in action lets transform our app into a Spring Authorization Server. All we need to do is add the Spring Authorization Server dependency ad then configure an OAuth client.
* RW: what's an OAuth?
* so copy and paste the requisite configuration:
```
spring:
security:
oauth2:
authorizationserver:
client:
oidc-client:
registration:
client-id: "spring"
client-secret: "spring"
client-authentication-methods:
- "client_secret_basic"
authorization-grant-types:
- "authorization_code"
- "refresh_token"
redirect-uris:
- "http://127.0.0.1:8081/login/oauth2/code/spring"
scopes:
- "openid"
- "profile"

```
* this establishes a new client called `oidc-client`. other apps will say they're connecting as this client _on behalf_ of a user context. now we need to standup a client. that client will have a filter that rejects requests and forces an authentication. which is of course what we've just spent a lot of time simplifying. lets build a new client.
* we'll also need a seperate spring authorization security filter chain, like this:
```
@Bean
@Order(1)
SecurityFilterChain authServerFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults());
http
.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
)
.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(Customizer.withDefaults()));

return http.build();
}

```
* restart. now we have an authorization server! nice. confirm as much by visiting `http://localhost:8080/.well-known/openid-configuration`. great.
* now we need a client.
* start.spring.io and create a new project called `oauth-login`: `security` , `web` , `oauth client`.
* configure some properties:
```
spring:

application:
name: oauth-login

security:
oauth2:
client:
registration:
spring:
scope: openid,profile
client-id: spring
client-secret: "{noop}spring"
provider:
spring:
issuer-uri: http://localhost:8080
server:
port: 8081

```
* add a simple endpoint to the oauth client.
```
@Controller
@ResponseBody
class ClientApplication {

@GetMapping("/")
Map hello(Principal principal) {
return Map.of("message", "hello, from client " + principal.getName());
}
}

```
* restart. visit the oauth client on port `8081`: `http://127.0.0.1:8081`. itll redirect us back to the auth server which in turn wil dump us back on the client, this time after having established a token. so were authenticated but the user never had to enter a pasword on the client. they can trust the auth server as the one true place. u see this scheme countles places. google. facebook. twitter. linkedin. github. etc.
* it works.
* now what if i have a downstream microservice to which the client wants to make calls? simple. sertup a resource server.
* start.spring.io: `resource server` , `web`, `graalvm`
* configure port: 8082
* configure issuer uri : http://localhost:8080
* add a controller in the resource server to be exactly the same as in the client:
```
@Controller
@ResponseBody
class ClientController {

@GetMapping("/")
Map hello(Principal principal) {
return Map.of("message", "hello, " + principal.getName());
}
}

```
* and now change the client to relay the token in the request:
```

@Controller
@ResponseBody
class ClientController {

private final RestClient http;

ClientController(RestClient.Builder http) {
this.http = http.build();
}

@GetMapping("/")
Map hello(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient oAuth2AuthorizedClient) {
var token = oAuth2AuthorizedClient.getAccessToken();
return http
.get()
.uri("http://localhost:8082/")
.headers(httpHeaders -> httpHeaders.setBearerAuth(token.getTokenValue()))
.retrieve()
.body(new ParameterizedTypeReference<>() {
});

}
}

```
* go back to the client as before: `127.0.0.1:8081/`