a dev's blog

Some thoughts about thoughts.

Spring-Boot-Security. Authenticate programmatically

2016-02-09 Development Java Spring

User Authentication mit Spring-Boot - und zwar programmatisch

Spring-Boot ist inzwischen ja recht bekannt (jaha, sehr suggestiv ;) ) für eine schicke Umgebung um in kurzer Zeit großartige Anwendungen auf den Weg zu bringen.

Ein Punkt, der bei einer herkömmlichen Webanwendung immer wieder Auftritt ist die programmatische Authentifizierung. Normalerweise loggt sich ein User in einer Webanwendung auf einer Login-Seite ein; oder es gibt eine Authentifizierung via OAuth, etc.pp. Allerdings gibt es - neben einer Login-Seite - häufig auch eine Register-Seite, die einen User nach erfolgreicher Authentifizierung auch einloggen sollte. Oder aber wir haben eine andere Plattform, die via PGP-verschlüsselter Token-Authentifizierung in der Lage sein soll Links zu generieren, die User in unserer Webanwendung anmelden soll.

Also. Auf gehts!

Setup

Zunächst einmal findet ihr ein voll funktionsfähiges Example auf meinem Github Account.

Für das Projekt-Setup reichen uns - abseits des Demo Projekts - aber folgende Maven Dependencies…

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>

…sowie eine SpringBootApplication:

@SpringBootApplication
public class Application {

  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }
}

Damit haben wir an sich schonmal eine lauffähige Applikation die allerdings noch keine Funktion besitzt.

Security

Als nächstes gehen wir mal davon aus, dass ihr in irgendeiner Weise einen abgesicherten Bereich in eurer Web-Applikation habt. Dies können wir einfach mit ein paar Zeilen in unserer Security-Config hinbekommen.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/", "/auth/**").permitAll()
        .antMatchers("/secured").hasRole("USER");
  }
}

Diese Security-Config erlaubt jedem den Zugriff auf unsere index-Resource / sowie unsere auth-Resource. Alle Requests gegen die Resource secured werden hingegen von Spring-Security abgesichert, so dass sie nur von eingeloggten Usern mit der Rolle USER erreichbar ist. Ansonsten wird Spring-Security eine 403 Errorpage leifern.

Das nächste, was wir brauchen ist eine Implementierung des Spring UserDetailsService, der uns eine Repräsentation unserer User in einer Form liefert, die mit Spring konform geht. Ein einfaches und ziemlich weit von der Realität entferntes Beispiel ist folgendes:

@Service
public class ExampleUserDetailsService implements UserDetailsService {
  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    return new User(username, "passwd", AuthorityUtils.createAuthorityList("ROLE_USER"));
  }
}

Hier wird einfach immer ein neuer User mit der Role USER erzeugt - und das jedes mal, wenn wir die Methode aufrufen. Klingt für produktionsverhältnisse nicht sonderlich richtig, ist uns aber für die Demo egal. ;)

Controller

Sodele. Jetzt machen wir uns unsere Controller, die wir oben bereits in der SecurityConfig verrechtet haben.

@Controller
public class ExampleController {

  @RequestMapping("/secured")
  @ResponseBody
  public String securedContent() {
    return "whooow secured content!!";
  }

  @RequestMapping("/")
  @ResponseBody
  public String index() {
    return "it works!";
  }
}

Ein Starten unserer Applikation sowie der aufruf von http://localhost:8080 liefert uns nun it works!. Ein Aufruf von http://localhost:8080/secured liefert uns hingegen eine 403.

Gehen wir jetzt mal davon aus, dass wir ein unglaublich sicheres Token besitzen würden und unsere Endpunkte natürlich nur via https verfügbar wären. Dann könnten wir uns in irgendeinem Controller einen neuen Endpunkt definieren, der uns anmeldet:

@Autowired
private ExampleUserDetailsService userDetailsService;

@RequestMapping("/auth")
public String auth(@RequestParam String token) {
  // This whole stuff should be inside of a service method...
  String usernameByToken = findUsernameByToken(token);

  UserDetails userDetails = userDetailsService.loadUserByUsername(usernameByToken);
  UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
  SecurityContextHolder.getContext().setAuthentication(authToken);

  LOG.info("User logged in. username={}, token={}", userDetails.getUsername(), token);

  return "redirect:/secured";
}

public String findUsernameByToken(String authToken) {
  return "myImpressiveUsername";
}

Und siehe da, nach einem Aufruf von http://localhost:8080/auth?token=foo sehen wir: whooow secured content!! :)

Fazit

Wir haben gesehen, dass es ziemlich einfach ist einen User im System anzumelden, und das auch an der Login-Seite des Systems vorbei geht.