Play Silhouette i logowanie przez Sociale
Większość użytkowników nie zadaje sobie trudu, aby wypełnić formularz ze swoim e-mailem, imieniem i nazwiskiem, tym samym loginem co na każdej innej stronie i (najtrudniejsza część) dwoma identycznymi, skomplikowanymi i silnymi hasłami. Następnie za każdym razem, gdy wracają na Twoją stronę, muszą się zalogować, co oznacza przypominanie sobie, którego hasła użyli. Jednym ze sposobów uproszczenia tego jest autoryzacja użytkowników do Twojej aplikacji za pomocą loginów społecznościowych.
Play i Silhouette
Silhouette to popularna biblioteka uwierzytelniania Scala dla Play Framework. Obsługuje wiele metod uwierzytelniania – w tym OAuth2. Zawiera również wiele wstępnie skonfigurowanych klas Provider dla popularnych serwisów społecznościowych, co minimalizuje ilość niezbędnej pracy.
Konfiguracja
Pierwszą rzeczą, którą musimy skonfigurować jest dostawca state parameter
. Będziemy go używać do zapobiegania atakom CSRF.
Pierwszym krokiem jest dodanie kilku kluczy konfiguracyjnych i wartości do silhouette.conf
. Nasz snippet poniżej przedstawia konfigurację zarówno dla CSRF state handler jak i dla OAuth2 state provider – ponieważ będziemy potrzebować ich obu.
# OAuth2 state provider settings oauth2StateProvider.cookieName="OAuth2TokenSecret" oauth2StateProvider.cookiePath="/" oauth2StateProvider.secureCookie=false oauth2StateProvider.httpOnlyCookie=true oauth2StateProvider.sameSite="Lax" oauth2StateProvider.expirationTime=5 minutes oauth2StateProvider.signer.key = "[changeme]" oauth2StateProvider.crypter.key = "[changeme]" # Social state handler socialStateHandler.signer.key = "[changeme]" # CSRF state item handler settings csrfStateItemHandler.cookieName="OAuth2State" csrfStateItemHandler.cookiePath="/" csrfStateItemHandler.secureCookie=false csrfStateItemHandler.httpOnlyCookie=true csrfStateItemHandler.sameSite="Lax" csrfStateItemHandler.expirationTime=5 minutes csrfStateItemHandler.signer.key = "[changeme]"
Signery
Następną rzeczą, którą należy zrobić, jest stworzenie Signer
f dla CSRF state item handler i OAuth2 state provider. Będą one używane do podpisywania ciasteczek po stronie klienta. Ta implementacja powinna wyglądać jak ta poniżej i powinna być umieszczona w tym samym pliku co reszta funkcji Silhouette np. SilhouetteModule.scala
.
/** * Provides the signer for the social state handler. * * @param configuration The Play configuration. * @return The signer for the social state handler. */ @Provides @Named("social-state-signer") def provideSocialStateSigner(configuration: Configuration): Signer = { val config = configuration.underlying.as[JcaSignerSettings]("silhouette.socialStateHandler.signer") new JcaSigner(config) } /** * Provides the signer for the CSRF state item handler. * * @param configuration The Play configuration. * @return The signer for the CSRF state item handler. */ @Provides @Named("csrf-state-item-signer") def provideCSRFStateItemSigner(configuration: Configuration): Signer = { val config = configuration.underlying.as[JcaSignerSettings]("silhouette.csrfStateItemHandler.signer") new JcaSigner(config) }
Dostawcy
Po utworzeniu Signer
, możemy stworzyć zarówno CSRF state item handler jak i social state handler. Najpierw stworzymy CSRF item handler.
/** * Provides the CSRF state item handler. * * @param idGenerator The ID generator implementation. * @param signer The signer implementation. * @param configuration The Play configuration. * @return The CSRF state item implementation. */ @Provides def provideCsrfStateItemHandler( idGenerator: IDGenerator, @Named("csrf-state-item-signer") signer: Signer, configuration: Configuration): CsrfStateItemHandler = { val settings = configuration.underlying.as[CsrfStateSettings]("silhouette.csrfStateItemHandler") new CsrfStateItemHandler(settings, idGenerator, signer) }
Teraz używając tego CSRF state item handler stworzymy social state handler. Użyjemy DefaultSocialStateHandler
ponieważ dokumentacja Silhouette sugeruje, że jest to najlepsza opcja.
/** * Provides the social state handler. * * @param signer The signer implementation. * @return The social state handler implementation. */ @Provides def provideSocialStateHandler( @Named("social-state-signer") signer: Signer, csrfStateItemHandler: CsrfStateItemHandler): SocialStateHandler = { new DefaultSocialStateHandler(Set(csrfStateItemHandler), signer) }
Dostawcy mediów społecznościowych
Teraz, gdy skonfigurowaliśmy Silhouette dla przepływu OAuth2 i zabezpieczyliśmy go przed atakami CSRF, możemy stworzyć naszych dostawców serwisów społecznościowych. Na potrzeby tego artykułu zaimplementujemy tylko dostawcę Google, ale inne konfiguracje są bardzo podobne. Możesz znaleźć przykładowe konfiguracje w dokumentacji Silhouette. Najprawdopodobniej będziesz chciał używać więcej niż jednego providera dla wygody swoich użytkowników. Kod przedstawiony w tej części artykułu będzie łatwo rozszerzalny.
Konfiguracja dostawcy
Pierwszą rzeczą, którą należy zrobić jest dodanie konfiguracji dostawcy do pliku silhouette.conf
. Dla Google powinien on wyglądać podobnie jak ten poniżej.
google { authorizationURL="https://accounts.google.com/o/oauth2/auth" accessTokenURL="https://accounts.google.com/o/oauth2/token" redirectURL=YOUR_APP_DOMAIN/authenticate/google" clientID="some_client_id" clientSecret="some_client_secret" scope="profile email" }
Uwaga: Musisz wygenerować clientID i clientSecret na swoim koncie Google.
Następnym krokiem jest stworzenie GoogleProvider
. Ponownie zrobimy to w klasie SilhouetteModule
. Poniższy snippet kodu przedstawia implementację, która wykorzysta konfigurację zapisaną w silhouette.conf
.
/** * Provides the Google provider. * * @param httpLayer The HTTP layer implementation. * @param socialStateHandler The social state handler implementation. * @param configuration The Play configuration. * @return The Google provider. */ @Provides def provideGoogleProvider( httpLayer: HTTPLayer, socialStateHandler: SocialStateHandler, configuration: Configuration): GoogleProvider = { new GoogleProvider(httpLayer, socialStateHandler, configuration.underlying.as[OAuth2Settings]("silhouette.google")) }
Teraz moglibyśmy użyć tylko tego providera w dalszej implementacji, ale dla łatwości rozszerzenia stworzymy SocialProviderRegistry
. Pozwoli nam to na łatwe dodawanie kolejnych providerów w przyszłości bez konieczności zmiany implementacji innych klas. Rejestr ten możemy stworzyć w następujący sposób:
/** * Provides the social provider registry. * * @param facebookProvider The Facebook provider implementation. * @param googleProvider The Google provider implementation. * @param linkedInProvider The LinkedIn provider implementation. * @return The Silhouette environment. */ @Provides def provideSocialProviderRegistry( googleProvider: GoogleProvider // add more providers here ): SocialProviderRegistry = { SocialProviderRegistry(Seq( googleProvider // add more providers here )) }
Kontroler uwierzytelniania
Teraz, gdy mamy już stworzonych providerów, musimy stworzyć punkt końcowy do uwierzytelniania. Najpierw, aby uzyskać dostęp do naszego repozytorium, musimy wstrzyknąć je do kontrolera, poprzez dodanie @Inject()(socialProviderRegistry: SocialProviderRegistry)
do klasy kontrolera. Kolejnym krokiem jest stworzenie Action
która będzie logować użytkowników, na podstawie danych przesłanych przez providera. Poniższa metoda pobiera ciąg provider
aby określić, który social provider został użyty przez naszego użytkownika i użyć tego właściwego. Następnie spróbuje uwierzytelnić użytkownika w oparciu o użycie wcześniej ustalonego providera. Jeśli zakończy się ona sukcesem (Right), pobierze dane z profilu przesłane przez dostawcę społecznościowego i zapisze je w bazie danych (userService.save(profile)
jest niestandardową funkcją, którą musisz stworzyć sam). Jeśli serwis zwróci Some(user) co w naszej implementacji oznacza, że dane zostały poprawnie zapisane, utworzy authenticator i opublikuje LoginEvent
skutecznie logując nowego użytkownika.
/** * Social log in * * @param provider login provider */ def socialAuthenticate(provider: String) = silhouette.UnsecuredAction.async { implicit request => (socialProviderRegistry.get[SocialProvider](provider) match { case Some(p: SocialProvider with CommonSocialProfileBuilder) => p.authenticate().flatMap { case Left(result) => Future.successful(result) // Return authentication result with error case Right(authInfo) => (for { profile <- p.retrieveProfile(authInfo) optionalUser <- userService.save(profile) } yield optionalUser match { case Some(user) => for { authenticator <- silhouette.env.authenticatorService.create(user.loginInfo) value <- silhouette.env.authenticatorService.init(authenticator) result <- silhouette.env.authenticatorService.embed(value, Redirect(routes.HomeController.index())) } yield { silhouette.env.eventBus.publish(LoginEvent(user, request)) result } case None => // Error handling while saving user Future.successful(Redirect(routes.AuthController.login).flashing("warning" -> Messages("auth.social.alreadyRegistered"))) }).flatten } case _ => Future.failed(new ProviderException(s"Cannot authenticate with unexpected social provider $provider")) // Wrong provider key }).recover { // Any other unforeseen error handling case e: ProviderException => logger.error("Unexpected provider error", e) Redirect(routes.AuthController.login).flashing("warning" -> Messages("auth.social.couldNotAuthenticate")) } }
Jak każdy endpoint musimy dodać go do konfiguracji routes
. Powinna ona wyglądać podobnie do tej:
GET /authenticate/:provider controllers.AuthController.socialAuthenticate(provider: String)
Szablon
Ostatnią rzeczą, którą należy zrobić jest umożliwienie użytkownikom dostępu do tej metody logowania. Zrobimy to dodając ten snippet do szablonu logowania.
@if(socialProviders.providers.nonEmpty) { <div class="social-providers mt-2"> <p class="col-sm-12 justify-content-center d-flex">@messages("auth.signIn.useSocial")</p> <div class="col-sm-12 justify-content-center d-flex"> @for(p <- socialProviders.providers) { <a href="@controllers.routes.AuthController.socialAuthenticate(p.id)" class="social-auth provider @p.id" title="@messages(p.id)"><i aria-hidden="true" class="fab fa-@{p.id} font-50 m-2"></i></a> } </div> </div> }
To stworzy linki do wszystkich dostawców społecznościowych z naszego Repozytorium. class="fab fa-@{p.id} font-50 m-2"
pobierze ikony dostawców z FontAwesome.