ECE366 - Lesson 9

Authentication

Instructor: Professor Hong

Review & Questions

## Axios
## What is Axios? Installation ``` npm install axios ``` - Another HTTP Client
## App.js with fetch ``` import "./App.css"; import { Link } from "react-router-dom"; import { useState, useEffect } from "react"; function Home() { const [data, setData] = useState(null); useEffect(() => { fetch( `http://localhost:8080/api/getUsers` ) .then((response) => response.json()) .then(setData); }, []); if (data) return ( <> <h1>RPS</h1> <nav> <Link to="/about">About</Link> <Link to="/contact">Contact</Link> </nav> <pre>{JSON.stringify(data, null, 2)}</pre> </> ); return <h1>Data</h1>; } export function About() { return ( <div> <nav> <Link to="/">Home</Link> <Link to="/about">About</Link> <Link to="/contact">Contact</Link> </nav> <h1>About Us</h1> </div> ); } export function Contact() { return ( <div> <nav> <Link to="/">Home</Link> <Link to="/about">About</Link> <Link to="/contact">Contact</Link> </nav> <h1>Contact Us</h1> </div> ); } export function App() { return <Home />; } ```
## App.js with Axios ``` function Home() { const [data, setData] = useState(null); useEffect(() => { const loadUsers = async () => { const response = await axios.get(`http://localhost:8080/api/getUsers`); setData(response.data) } loadUsers(); }, []); if (data) return ( <> <h1>RPS</h1> <nav> <Link to="/about">About</Link> <Link to="/contact">Contact</Link> </nav> <pre>{JSON.stringify(data, null, 2)}</pre> </> ); return <h1>Data</h1>; } ```
## Authentication
## Firebase Installation ``` npm install firebase ```
## Setup Firebase - Go to [https://console.firebase.google.com/](https://console.firebase.google.com/) - Create a new project - No need to select Google analytics - Add firebase to your web app by clicking Web - Insert public keys to index.js - Get Started w/ Authentication - Enable Email/Password and save - Add a sample user
## Login Page ``` import {useState} from "react"; import {Link, useNavigate} from "react-router-dom"; import {getAuth, signInWithEmailAndPassword} from 'firebase/auth'; const LoginPage = () => { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(""); const navigate = useNavigate(); const logIn = async () => { try { await signInWithEmailAndPassword(getAuth(), email, password); navigate('/'); } catch (e) { setError(e.message); } }; return ( <> <h1>Log In</h1> {error && <p className="error">{error}</p>} <input placeholder="Your email address" value={email} onChange={e=>setEmail(e.target.value)} /> <input type="password" placeholder="Your password" value={password} onChange={e=>setPassword(e.target.value)} /> <button onClick={logIn}>Log In</button> <Link to="/create-account">Don't have an account? Create one here</Link> </> ); } export default LoginPage; ``` - Calling firebase to log in and receive a token for authentication
## Create Account Page ``` import {useState} from "react"; import {Link, useNavigate} from "react-router-dom"; import {getAuth, createUserWithEmailAndPassword} from "firebase/auth"; const CreateAccountPage = () => { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); const [error, setError] = useState(""); const navigate = useNavigate(); const createAccount = async () => { try { if (password !== confirmPassword) { setError("Password and confirm password do not match"); return; } await createUserWithEmailAndPassword(getAuth(), email, password); navigate("/"); } catch(e) { setError(e.message); } }; return ( <> <h1>Create Account</h1> {error && <p className="error">{error}</p>} <input placeholder="Your email address" value={email} onChange={e=>setEmail(e.target.value)} /> <input type="password" placeholder="Your password" value={password} onChange={e=>setPassword(e.target.value)} /> <input type="password" placeholder="Re-enter your password" value={confirmPassword} onChange={e=>setConfirmPassword(e.target.value)} /> <button onClick={createAccount}>Log In</button> <Link to="/login">Already have an account? Log in here.</Link> </> ); } export default CreateAccountPage; ``` - Similar to log in, but with a different function
## useUser Hook hooks/useUser.js ``` // custom hook import {useState, useEffect} from "react"; import { getAuth, onAuthStateChanged} from "firebase/auth"; const useUser = () => { const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { const unsubscribe = onAuthStateChanged(getAuth(), user => { setUser(user); setIsLoading(false); }); return unsubscribe; // if user navigates away, removes hook }, []); // only calls this when auth; only once return {user, isLoading}; }; export default useUser; ``` - This gets the user info and checks if the screen is still loading
## Restrict User from Viewing Info ``` import useUser from "./hooks/useUser"; function Home() { const [data, setData] = useState(null); const {user, isLoading} = useUser(); useEffect(() => { const loadUsers = async () => { const response = await axios.get(`http://localhost:8080/api/getUsers`); setData(response.data) } loadUsers(); }, [isLoading, user]); if (data) return ( <> <h1>RPS</h1> <nav> <Link to="/about">About</Link> <Link to="/contact">Contact</Link> <Link to="/login">Login</Link> </nav> {user ? <pre>{JSON.stringify(data, null, 2)}</pre> : <p>Log in to view sensitive info!</p> } </> ); return <h1>Data</h1>; } ``` - Note the conditional for the display of data
## Logging Out ``` import { Link, useNavigate } from "react-router-dom"; import {getAuth, signOut} from 'firebase/auth'; import useUser from "./hooks/useUser"; function Home() { const [data, setData] = useState(null); const {user, isLoading} = useUser(); const navigate = useNavigate(); useEffect(() => { const loadUsers = async () => { const response = await axios.get(`http://localhost:8080/api/getUsers`); setData(response.data) } loadUsers(); }, [isLoading, user]); if (data) return ( <> <h1>RPS</h1> <nav> <Link to="/about">About</Link> <Link to="/contact">Contact</Link> <Link to="/login">Login</Link> {user ? <button onClick={() => { signOut(getAuth()); }}>Log Out</button> : <button onClick={() => { navigate('/login') }}>Log In</button> } </nav> {user ? <pre>{JSON.stringify(data, null, 2)}</pre> : <p>Log in to view sensitive info!</p> } </> ); return <h1>Data</h1>; } ```
## Authentication in Spring Boot
## Add Dependencies ``` <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> ``` resources/application.properties ``` spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://www.googleapis.com/service_accounts/v1/jwk/securetoken%40system.gserviceaccount.com ```
## WebSecurityConfiguration config/WebSecurityConfiguration.java ``` package com.cooperps.rps.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; import static org.springframework.security.config.Customizer.withDefaults; @Configuration public class WebSecurityConfiguration { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests((authz) -> authz .anyRequest().authenticated() ) .httpBasic(withDefaults()); http.oauth2ResourceServer() .jwt(); return http.build(); } } ```
## Add AppController to test web/AppController.java ``` package com.cooperps.rps.web; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.security.Principal; @RestController @RequestMapping("/app") public class AppController { @GetMapping(path = "/test") public String test(Principal principal) { return principal.getName(); } } ```
## Pass token to Server ``` function Home() { const [data, setData] = useState(null); const {user, isLoading} = useUser(); const navigate = useNavigate(); useEffect(() => { const loadUsers = async () => { const token = user && await user.getIdToken(); console.log(token); const headers = token ? {Authorization: `Bearer ${token}`} : {}; const response = await axios.get(`http://localhost:8080/api/getUsers`, {headers}); setData(response.data) } if(!isLoading) { loadUsers(); } }, [isLoading, user]); if (data) return ( <> <h1>RPS</h1> <nav> <Link to="/about">About</Link> <Link to="/contact">Contact</Link> <Link to="/login">Login</Link> {user ? <button onClick={() => { signOut(getAuth()); }}>Log Out</button> : <button onClick={() => { navigate('/login') }}>Log In</button> } </nav> {user ? <pre>{JSON.stringify(data, null, 2)}</pre> : <p>Log in to view sensitive info!</p> } </> ); return ( <> <h1>RPS</h1> <nav> <Link to="/about">About</Link> <Link to="/contact">Contact</Link> <Link to="/login">Login</Link> {user ? <button onClick={() => { signOut(getAuth()); }}>Log Out</button> : <button onClick={() => { navigate('/login') }}>Log In</button> } </nav>; </> ); } ```
## CORS again package.json ``` "proxy": "http://localhost:8080/", ``` and restart with ``` npm start ```
## Websocket + STOMP
## STOMP - Simple/Streaming Text Oriented Message Protocol - Allows you to create an interactive web application - The web is the STOMP client - The service is the message broker - Example from [https://spring.io/guides/gs/messaging-stomp-websocket/](https://spring.io/guides/gs/messaging-stomp-websocket/)
## Pre-initialized Spring Initializr [Pre-initialized Initializr](https://start.spring.io/#!type=maven-project&groupId=com.example&artifactId=messaging-stomp-websocket&name=messaging-stomp-websocket&description=Demo%20project%20for%20Spring%20Boot&packageName=com.example.messaging-stomp-websocket&dependencies=websocket) Additional pom.xml dependencies ``` <dependency> <groupId>org.webjars</groupId> <artifactId>webjars-locator-core</artifactId> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>sockjs-client</artifactId> <version>1.0.2</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>stomp-websocket</artifactId> <version>2.3.3</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>bootstrap</artifactId> <version>3.3.7</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>3.1.1-1</version> </dependency> ```
## HelloMessage Class HelloMessage.java ``` package com.example.messagingstompwebsocket; public class HelloMessage { private String name; public HelloMessage() { } public HelloMessage(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } } ```
## Greeting Class Greeting.java ``` package com.example.messagingstompwebsocket; public class Greeting { private String content; public Greeting() { } public Greeting(String content) { this.content = content; } public String getContent() { return content; } } ```
## GreetingController Class GreetingController.java ``` package com.example.messagingstompwebsocket; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.stereotype.Controller; import org.springframework.web.util.HtmlUtils; @Controller public class GreetingController { @MessageMapping("/hello") @SendTo("/topic/greetings") public Greeting greeting(HelloMessage message) throws Exception { Thread.sleep(1000); // simulated delay return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!"); } } ```
## WebSocketConfig Class WebSocketConfig.java ``` package com.example.messagingstompwebsocket; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/topic"); config.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/gs-guide-websocket").withSockJS(); } } ```
## Index.html src/main/resources/static/index.html ``` <!DOCTYPE html> <html> <head> <title>Hello WebSocket</title> <link href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet"> <link href="/main.css" rel="stylesheet"> <script src="/webjars/jquery/jquery.min.js"></script> <script src="/webjars/sockjs-client/sockjs.min.js"></script> <script src="/webjars/stomp-websocket/stomp.min.js"></script> <script src="/app.js"></script> </head> <body> <noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being enabled. Please enable Javascript and reload this page!</h2></noscript> <div id="main-content" class="container"> <div class="row"> <div class="col-md-6"> <form class="form-inline"> <div class="form-group"> <label for="connect">WebSocket connection:</label> <button id="connect" class="btn btn-default" type="submit">Connect</button> <button id="disconnect" class="btn btn-default" type="submit" disabled="disabled">Disconnect </button> </div> </form> </div> <div class="col-md-6"> <form class="form-inline"> <div class="form-group"> <label for="name">What is your name?</label> <input type="text" id="name" class="form-control" placeholder="Your name here..."> </div> <button id="send" class="btn btn-default" type="submit">Send</button> </form> </div> </div> <div class="row"> <div class="col-md-12"> <table id="conversation" class="table table-striped"> <thead> <tr> <th>Greetings</th> </tr> </thead> <tbody id="greetings"> </tbody> </table> </div> </div> </div> </body> </html> ```
## app.js src/main/resources/static/app.html ``` var stompClient = null; function setConnected(connected) { $("#connect").prop("disabled", connected); $("#disconnect").prop("disabled", !connected); if (connected) { $("#conversation").show(); } else { $("#conversation").hide(); } $("#greetings").html(""); } function connect() { var socket = new SockJS('/gs-guide-websocket'); stompClient = Stomp.over(socket); stompClient.connect({}, function (frame) { setConnected(true); console.log('Connected: ' + frame); stompClient.subscribe('/topic/greetings', function (greeting) { showGreeting(JSON.parse(greeting.body).content); }); }); } function disconnect() { if (stompClient !== null) { stompClient.disconnect(); } setConnected(false); console.log("Disconnected"); } function sendName() { stompClient.send("/app/hello", {}, JSON.stringify({'name': $("#name").val()})); } function showGreeting(message) { $("#greetings").append("" + message + ""); } $(function () { $("form").on('submit', function (e) { e.preventDefault(); }); $( "#connect" ).click(function() { connect(); }); $( "#disconnect" ).click(function() { disconnect(); }); $( "#send" ).click(function() { sendName(); }); }); ```
## RPS Game Logic - Add server side logic - Add UI to communicate with server