├── .github
└── dco.yml
├── .gitignore
├── .mvn
└── wrapper
│ ├── maven-wrapper.jar
│ └── maven-wrapper.properties
├── .travis.yml
├── CONTRIBUTING.adoc
├── Gemfile
├── Guardfile
├── LICENSE.code.txt
├── LICENSE.writing.txt
├── README.adoc
├── click
├── README.adoc
├── pom.xml
└── src
│ ├── main
│ ├── java
│ │ └── com
│ │ │ └── example
│ │ │ └── SocialApplication.java
│ └── resources
│ │ ├── application.yml
│ │ └── static
│ │ └── index.html
│ └── test
│ └── java
│ └── com
│ └── example
│ └── SocialApplicationTests.java
├── custom-error
├── README.adoc
├── pom.xml
└── src
│ ├── main
│ ├── java
│ │ └── com
│ │ │ └── example
│ │ │ └── SocialApplication.java
│ └── resources
│ │ ├── application.yml
│ │ └── static
│ │ └── index.html
│ └── test
│ └── java
│ └── com
│ └── example
│ └── SocialApplicationTests.java
├── logout
├── README.adoc
├── pom.xml
└── src
│ ├── main
│ ├── java
│ │ └── com
│ │ │ └── example
│ │ │ └── SocialApplication.java
│ └── resources
│ │ ├── application.yml
│ │ └── static
│ │ └── index.html
│ └── test
│ └── java
│ └── com
│ └── example
│ └── SocialApplicationTests.java
├── mvnw
├── mvnw.cmd
├── overview.adoc
├── pom.xml
├── simple
├── README.adoc
├── pom.xml
└── src
│ ├── main
│ ├── java
│ │ └── com
│ │ │ └── example
│ │ │ └── SocialApplication.java
│ └── resources
│ │ ├── application.yml
│ │ └── static
│ │ └── index.html
│ └── test
│ └── java
│ └── com
│ └── example
│ └── SocialApplicationTests.java
└── two-providers
├── README.adoc
├── pom.xml
└── src
├── main
├── java
│ └── com
│ │ └── example
│ │ └── SocialApplication.java
└── resources
│ ├── application.yml
│ └── static
│ └── index.html
└── test
└── java
└── com
└── example
└── SocialApplicationTests.java
/.github/dco.yml:
--------------------------------------------------------------------------------
1 | require:
2 | members: false
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | *.iml
3 | .factorypath
4 | classes
5 | target
6 | bin/
7 | .classpath
8 | .project
9 | .settings/
10 | Gemfile.lock
11 |
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spring-guides/tut-spring-boot-oauth2/c827fc8739f9e2658fced818bf8c6a40af7b9256/.mvn/wrapper/maven-wrapper.jar
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.3/apache-maven-3.3.3-bin.zip
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | cache:
2 | directories:
3 | - $HOME/.m2
4 | jdk:
5 | - openjdk8
6 | language: java
7 | script: ./mvnw --fail-at-end --update-snapshots clean install
8 | notifications:
9 | slack:
10 | secure: oba1C58doySmrOhh1vFxccgp9JCQi1cNBKhC0YD5I9uk44eXAsOs8GjIs3wqfReKkSf4e9N8jRpbQWoIayQV/qRvkRJnpxuFv5q3Rtr4+S50Ie2IfrghRpphV7uyL3ktswyPHLTLVNxEW1cext9ZDOMb3S+Z4lwCz0gBjf9cZWGzBIsj6Wb0cr31Jo/xKuvBTwxRdTJGh20CvHXpXYeB/dNbY5BmlDP1Ys8dtP0/NrWTJ5/MG2BubaQ/xuUA+aw573XaYA+3t6vDVEfedNcDO8oNcQgp9zpp3p5Z4pipqKGyrqQAp/xnRt73TGRl6XQ9t/ZGdOSz6vSHaaVhtBKkOpoW8DxQ9nq9GWOK7R9r1ok919blzSfZvjJ5jUVM931XaUhIfWbv1f/UGgFRJSJ/G81s27trd0GCYXPXyq1tFVQBp/CxrBwuOfPtE7rTwFaFM4eo6tgwkbHFW4ZXJWk5MXWhkgRM7Frqq5mMophz6YT2suuEYgYVM7f0l2lXAC/O7PkobMzbeDRiMwREx5KBKGd6GFpmKfUua8b75nOcn9UB4aDtdCb70LwBCVotvQT0rKk1zxLp28iNilk1HQubfOG9S5gjjuqsXPfHgsmzA2CnQbj1yjansGXA8uq0NBOBUo2lvopsXGBw0tJNj53DY5sxd3jXaNIYQjsGT2knHp0=
11 |
--------------------------------------------------------------------------------
/CONTRIBUTING.adoc:
--------------------------------------------------------------------------------
1 | All commits must include a __Signed-off-by__ trailer at the end of each commit message to indicate that the contributor agrees to the Developer Certificate of Origin.
2 | For additional details, please refer to the blog post https://spring.io/blog/2025/01/06/hello-dco-goodbye-cla-simplifying-contributions-to-spring[Hello DCO, Goodbye CLA: Simplifying Contributions to Spring].
3 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gem "guard"
4 | gem "guard-shell"
5 | gem "asciidoctor"
6 |
--------------------------------------------------------------------------------
/Guardfile:
--------------------------------------------------------------------------------
1 | require 'asciidoctor'
2 | require 'erb'
3 |
4 | options = {:mkdirs => true, :safe => :unsafe, :attributes => ['linkcss', 'allow-uri-read']}
5 |
6 | guard 'shell' do
7 | watch('.*.adoc') {|m|
8 | Asciidoctor.render_file('README.adoc', options.merge(:to_dir => 'target/generated-docs'))
9 | }
10 | end
11 |
--------------------------------------------------------------------------------
/LICENSE.code.txt:
--------------------------------------------------------------------------------
1 | All code in this repository is:
2 | =======================================================================
3 | Copyright (c) 2013 GoPivotal, Inc. All Rights Reserved
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | https://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 |
17 |
--------------------------------------------------------------------------------
/LICENSE.writing.txt:
--------------------------------------------------------------------------------
1 | Except where otherwise noted, this work is licensed under https://creativecommons.org/licenses/by-nd/3.0/
2 |
--------------------------------------------------------------------------------
/README.adoc:
--------------------------------------------------------------------------------
1 | ---
2 | tags: [security,javascript,rest,oauth]
3 | projects: [spring-security,spring-security-oauth,spring-boot]
4 | ---
5 | :toc: left
6 | :icons: font
7 | :source-highlighter: prettify
8 | :image-width: 500
9 | :doctype: book
10 | :star: {asterisk}
11 | :all: {asterisk}{asterisk}
12 |
13 | = Social Login with Spring Boot and OAuth 2.0
14 |
15 | This guide shows you how to build a sample app doing various things with "social login" using https://tools.ietf.org/html/rfc6749[OAuth 2.0] and https://projects.spring.io/spring-boot/[Spring Boot].
16 |
17 | It starts with a simple, single-provider single-sign on, and works up to a client with a choice of authentication providers:
18 | https://github.com/settings/developers[GitHub] or https://developers.google.com/identity/protocols/OpenIDConnect[Google].
19 |
20 | The samples are all single-page apps using Spring Boot and Spring Security on the back end.
21 | They also all use plain https://jquery.org/[jQuery] on the front end.
22 | But, the changes needed to convert to a different JavaScript framework or to use server-side rendering would be minimal.
23 |
24 | All samples are implemented using the native OAuth 2.0 support in https://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#boot-features-security-oauth2[Spring Boot].
25 |
26 | include::overview.adoc[]
27 |
28 | include::simple/README.adoc[leveloffset=+1]
29 | include::click/README.adoc[leveloffset=+1]
30 | include::logout/README.adoc[leveloffset=+1]
31 | include::two-providers/README.adoc[leveloffset=+1]
32 | include::custom-error/README.adoc[leveloffset=+1]
33 |
34 | == Conclusion
35 |
36 | We have seen how to use Spring Boot and Spring Security to build apps in a number of styles with very little effort.
37 | The main theme running through all of the samples is authentication using an external OAuth 2.0 provider.
38 |
39 | All of the sample apps can be easily extended and re-configured for more specific use cases, usually with nothing more than a configuration file change.
40 | Remember if you use versions of the samples in your own servers to register with GitHub (or similar) and get client credentials for your own host addresses.
41 | And remember not to put those credentials in source control!
42 |
43 | include::https://raw.githubusercontent.com/spring-guides/getting-started-macros/master/footer.adoc[]
44 |
--------------------------------------------------------------------------------
/click/README.adoc:
--------------------------------------------------------------------------------
1 | [[_social_login_click]]
2 | = Add a Welcome Page
3 |
4 | In this section, you'll modify the <<_social_login_simple,simple>> app you just built by adding an explicit link to login with GitHub.
5 | Instead of being redirected immediately, the new link will be visible on the home page, and the user can choose to login or to stay unauthenticated.
6 | Only when the user has clicked on the link will the secure content be rendered.
7 |
8 | == Conditional Content on the Home Page
9 |
10 | To render content on the condition that the user is authenticated, you have the option of either server-side or client-side rendering.
11 |
12 | Here, you'll change the client side with https://jquery.org/[JQuery], though if you prefer to use something else, it shouldn't be very hard to translate the client code.
13 |
14 | To get started with the dynamic content, you need to mark a couple of HTML elements like so:
15 |
16 | .index.html
17 | [source,html]
18 | ----
19 |
` will show, and the second one won't.
28 | Note also the empty `` with an `id` attribute.
29 |
30 | In a moment, you'll add a server-side endpoint that will return the logged in user details as JSON.
31 |
32 | But, first, add the following JavaScript, which will hit that endpoint.
33 | Based on the endpoint's response, this JavaScript will populate the `` tag with the user's name and toggle the `
` appropriately:
34 |
35 | .index.html
36 | [source,html]
37 | ----
38 |
45 | ----
46 |
47 | Note that this JavaScript expects the server-side endpoint to be called `/user`.
48 |
49 | == The `/user` Endpoint
50 |
51 | Now, you'll add the server-side endpoint just mentioned, calling it `/user`.
52 | It will send back the currently logged-in user, which we can do quite easily in our main class:
53 |
54 | .SocialApplication.java
55 | [source,java]
56 | ----
57 | @SpringBootApplication
58 | @RestController
59 | public class SocialApplication {
60 |
61 | @GetMapping("/user")
62 | public Map user(@AuthenticationPrincipal OAuth2User principal) {
63 | return Collections.singletonMap("name", principal.getAttribute("name"));
64 | }
65 |
66 | public static void main(String[] args) {
67 | SpringApplication.run(SocialApplication.class, args);
68 | }
69 |
70 | }
71 | ----
72 |
73 | Note the use of `@RestController`, `@GetMapping`, and the `OAuth2User` injected into the handler method.
74 |
75 | WARNING: It's not a great idea to return a whole `OAuth2User` in an endpoint since it might contain information you would rather not reveal to a browser client.
76 |
77 | == Making the Home Page Public
78 |
79 | There's one final change you'll need to make.
80 |
81 | This app will now work fine and authenticate as before, but it's still going to redirect before showing the page.
82 | To make the link visible, we also need to switch off the security on the home page by extending `WebSecurityConfigurerAdapter`:
83 |
84 | .SocialApplication
85 | [source,java]
86 | ----
87 | @SpringBootApplication
88 | @RestController
89 | public class SocialApplication extends WebSecurityConfigurerAdapter {
90 |
91 | // ...
92 |
93 | @Override
94 | protected void configure(HttpSecurity http) throws Exception {
95 | // @formatter:off
96 | http
97 | .authorizeRequests(a -> a
98 | .antMatchers("/", "/error", "/webjars/**").permitAll()
99 | .anyRequest().authenticated()
100 | )
101 | .exceptionHandling(e -> e
102 | .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
103 | )
104 | .oauth2Login();
105 | // @formatter:on
106 | }
107 |
108 | }
109 | ----
110 |
111 | Spring Boot attaches special meaning to a `WebSecurityConfigurerAdapter` on the class annotated with `@SpringBootApplication`:
112 | It uses it to configure the security filter chain that carries the OAuth 2.0 authentication processor.
113 |
114 | The above configuration indicates a whitelist of permitted endpoints, with every other endpoint requiring authentication.
115 |
116 | You want to allow:
117 |
118 | * `/` since that's the page you just made dynamic, with some of its content visible to unauthenticated users
119 | * `/error` since that's a Spring Boot endpoint for displaying errors, and
120 | * `/webjars/**` since you'll want your JavaScript to run for all visitors, authenticated or not
121 |
122 | You won't see anything about `/user` in this configuration, though.
123 | Everything, including `/user` remains secure unless indicated because of the `.anyRequest().authenticated()` configuration at the end.
124 |
125 | Finally, since we are interfacing with the backend over Ajax, we'll want to configure endpoints to respond with a 401 instead of the default behavior of redirecting to a login page.
126 | Configuring the `authenticationEntryPoint` achieves this for us.
127 |
128 | With those changes in place, the application is complete, and if you run it and visit the home page you should see a nicely styled HTML link to "login with GitHub".
129 | The link takes you not directly to GitHub, but to the local path that processes the authentication (and sends a redirect to GitHub).
130 | Once you have authenticated, you get redirected back to the local app, where it now displays your name (assuming you have set up your permissions in GitHub to allow access to that data).
131 |
--------------------------------------------------------------------------------
/click/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | com.example
7 | social-click
8 | 0.0.1-SNAPSHOT
9 | jar
10 |
11 | social-click
12 | Demo project for Spring Boot
13 |
14 |
15 | org.springframework.boot
16 | spring-boot-starter-parent
17 | 2.2.2.RELEASE
18 |
19 |
20 |
21 |
22 | UTF-8
23 | 1.8
24 |
25 |
26 |
27 |
28 | org.springframework.boot
29 | spring-boot-starter-oauth2-client
30 |
31 |
32 | org.springframework.boot
33 | spring-boot-starter-web
34 |
35 |
36 | org.webjars
37 | jquery
38 | 2.1.1
39 |
40 |
41 | org.webjars
42 | bootstrap
43 | 3.2.0
44 |
45 |
46 | org.webjars
47 | webjars-locator-core
48 |
49 |
50 |
51 | org.springframework.boot
52 | spring-boot-starter-test
53 | test
54 |
55 |
56 |
57 |
58 |
59 |
60 | org.springframework.boot
61 | spring-boot-maven-plugin
62 |
63 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/click/src/main/java/com/example/SocialApplication.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2012-2015 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.example;
17 |
18 | import java.util.Collections;
19 | import java.util.Map;
20 |
21 | import org.springframework.boot.SpringApplication;
22 | import org.springframework.boot.autoconfigure.SpringBootApplication;
23 | import org.springframework.http.HttpStatus;
24 | import org.springframework.security.config.annotation.web.builders.HttpSecurity;
25 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
26 | import org.springframework.security.core.annotation.AuthenticationPrincipal;
27 | import org.springframework.security.oauth2.core.user.OAuth2User;
28 | import org.springframework.security.web.authentication.HttpStatusEntryPoint;
29 | import org.springframework.web.bind.annotation.GetMapping;
30 | import org.springframework.web.bind.annotation.RestController;
31 |
32 | @SpringBootApplication
33 | @RestController
34 | public class SocialApplication extends WebSecurityConfigurerAdapter {
35 |
36 | @GetMapping("/user")
37 | public Map user(@AuthenticationPrincipal OAuth2User principal) {
38 | return Collections.singletonMap("name", principal.getAttribute("name"));
39 | }
40 |
41 | @Override
42 | protected void configure(HttpSecurity http) throws Exception {
43 | // @formatter:off
44 | http
45 | .authorizeRequests(a -> a
46 | .antMatchers("/", "/error", "/webjars/**").permitAll()
47 | .anyRequest().authenticated()
48 | )
49 | .exceptionHandling(e -> e
50 | .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
51 | )
52 | .oauth2Login();
53 | // @formatter:on
54 | }
55 |
56 | public static void main(String[] args) {
57 | SpringApplication.run(SocialApplication.class, args);
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/click/src/main/resources/application.yml:
--------------------------------------------------------------------------------
1 | spring:
2 | security:
3 | oauth2:
4 | client:
5 | registration:
6 | github:
7 | client-id: your-github-client-id
8 | client-secret: your-github-client-secret
9 |
10 | #logging:
11 | # level:
12 | # org.springframework.security: DEBUG
13 |
--------------------------------------------------------------------------------
/click/src/main/resources/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Demo
7 |
8 |
9 |
10 |
12 |
13 |
15 |
16 |
17 |
24 |
31 |
32 |
--------------------------------------------------------------------------------
/click/src/test/java/com/example/SocialApplicationTests.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2012-2015 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.example;
17 |
18 | import org.junit.Test;
19 | import org.junit.runner.RunWith;
20 | import org.springframework.boot.test.context.SpringBootTest;
21 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
22 |
23 | @RunWith(SpringJUnit4ClassRunner.class)
24 | @SpringBootTest
25 | public class SocialApplicationTests {
26 |
27 | @Test
28 | public void contextLoads() {
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/custom-error/README.adoc:
--------------------------------------------------------------------------------
1 | [[_custom_error]]
2 | = Adding an Error Page for Unauthenticated Users
3 |
4 | In this section, you'll modify the <<_social_login_two_providers,two-providers>> app you built earlier to give some feedback to users that cannot authenticate.
5 | At the same time you'll extend the authentication logic to include a rule that only allows users if they belong to a specific GitHub organization.
6 | The "organization" is a GitHub domain-specific concept, but similar rules could be devised for other providers.
7 | For example, with Google you might want to only authenticate users from a specific domain.
8 |
9 | == Switching to GitHub
10 |
11 | The <<_social_login_two_providers,two-providers>> sample uses GitHub as an OAuth 2.0 provider:
12 |
13 | .application.yml
14 | [source,yaml]
15 | ----
16 | spring:
17 | security:
18 | oauth2:
19 | client:
20 | registration:
21 | github:
22 | client-id: bd1c0a783ccdd1c9b9e4
23 | client-secret: 1a9030fbca47a5b2c28e92f19050bb77824b5ad1
24 | # ...
25 | ----
26 |
27 | == Detecting an Authentication Failure in the Client
28 |
29 | On the client, you might like to provide some feedback for a user that could not authenticate.
30 | To facilitate this, you can add a div to which you'll eventually add an informative message.
31 |
32 | .index.html
33 | ----
34 |
35 | ----
36 |
37 | Then, add a call to the `/error` endpoint, populating the `
` with the result:
38 |
39 | .index.html
40 | ----
41 | $.get("/error", function(data) {
42 | if (data) {
43 | $(".error").html(data);
44 | } else {
45 | $(".error").html('');
46 | }
47 | });
48 | ----
49 |
50 | The error function checks with the backend if there is any error to display
51 |
52 | == Adding an Error Message
53 |
54 | To support the retrieval of an error message, you'll need to capture it when authentication fails.
55 | To achieve this, you can configure an `AuthenticationFailureHandler`, like so:
56 |
57 | [source,java]
58 | ----
59 | protected void configure(HttpSecurity http) throws Exception {
60 | // @formatter:off
61 | http
62 | // ... existing configuration
63 | .oauth2Login(o -> o
64 | .failureHandler((request, response, exception) -> {
65 | request.getSession().setAttribute("error.message", exception.getMessage());
66 | handler.onAuthenticationFailure(request, response, exception);
67 | })
68 | );
69 | }
70 | ----
71 |
72 | The above will save an error message to the session whenever authentication fails.
73 |
74 | Then, you can add a simple `/error` controller, like this one:
75 |
76 | .SocialApplication.java
77 | [source,java]
78 | ----
79 | @GetMapping("/error")
80 | public String error(HttpServletRequest request) {
81 | String message = (String) request.getSession().getAttribute("error.message");
82 | request.getSession().removeAttribute("error.message");
83 | return message;
84 | }
85 | ----
86 |
87 | NOTE: This will replace the default `/error` page in the app, which is fine for our case, but may not be sophisticated enough for your needs.
88 |
89 | == Generating a 401 in the Server
90 |
91 | A 401 response will already be coming from Spring Security if the user cannot or does not want to login with GitHub, so the app is already working if you fail to authenticate (e.g. by rejecting the token grant).
92 |
93 | To spice things up a bit, you can extend the authentication rule to reject users that are not in the right organization.
94 |
95 | You can use the GitHub API to find out more about the user, so you'll just need to plug that into the right part of the authentication process.
96 |
97 | Fortunately, for such a simple use case, Spring Boot has provided an easy extension point:
98 | If you declare a `@Bean` of type `OAuth2UserService`, it will be used to identify the user principal.
99 | You can use that hook to assert the the user is in the correct organization, and throw an exception if not:
100 |
101 | .SocialApplication.java
102 | [source,java]
103 | ----
104 | @Bean
105 | public OAuth2UserService oauth2UserService(WebClient rest) {
106 | DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
107 | return request -> {
108 | OAuth2User user = delegate.loadUser(request);
109 | if (!"github".equals(request.getClientRegistration().getRegistrationId())) {
110 | return user;
111 | }
112 |
113 | OAuth2AuthorizedClient client = new OAuth2AuthorizedClient
114 | (request.getClientRegistration(), user.getName(), request.getAccessToken());
115 | String url = user.getAttribute("organizations_url");
116 | List