gitlab.rs 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. //!
  2. //! This example showcases the process of integrating with the
  3. //! [GitLab OpenID Connect](https://docs.gitlab.com/ee/integration/openid_connect_provider.html)
  4. //! provider.
  5. //!
  6. //! Before running it, you'll need to generate your own
  7. //! [GitLab Application](https://docs.gitlab.com/ee/integration/oauth_provider.html).
  8. //! The application needs `openid`, `profile` and `email` permission.
  9. //!
  10. //! In order to run the example call:
  11. //!
  12. //! ```sh
  13. //! GITLAB_CLIENT_ID=xxx GITLAB_CLIENT_SECRET=yyy cargo run --example gitlab
  14. //! ```
  15. //!
  16. //! ...and follow the instructions.
  17. //!
  18. use std::env;
  19. use std::io::{BufRead, BufReader, Write};
  20. use std::net::TcpListener;
  21. use std::process::exit;
  22. use url::Url;
  23. use openidconnect::core::{
  24. CoreClient, CoreGenderClaim, CoreIdTokenClaims, CoreIdTokenVerifier, CoreProviderMetadata,
  25. CoreResponseType,
  26. };
  27. use openidconnect::reqwest::http_client;
  28. use openidconnect::{AdditionalClaims, UserInfoClaims};
  29. use openidconnect::{
  30. AuthenticationFlow, AuthorizationCode, ClientId, ClientSecret, CsrfToken, IssuerUrl, Nonce,
  31. OAuth2TokenResponse, RedirectUrl, Scope,
  32. };
  33. use serde::{Deserialize, Serialize};
  34. #[derive(Debug, Deserialize, Serialize)]
  35. struct GitLabClaims {
  36. // Deprecated and thus optional as it might be removed in the futre
  37. sub_legacy: Option<String>,
  38. groups: Vec<String>,
  39. }
  40. impl AdditionalClaims for GitLabClaims {}
  41. fn handle_error<T: std::error::Error>(fail: &T, msg: &'static str) {
  42. let mut err_msg = format!("ERROR: {}", msg);
  43. let mut cur_fail: Option<&dyn std::error::Error> = Some(fail);
  44. while let Some(cause) = cur_fail {
  45. err_msg += &format!("\n caused by: {}", cause);
  46. cur_fail = cause.source();
  47. }
  48. println!("{}", err_msg);
  49. exit(1);
  50. }
  51. fn main() {
  52. env_logger::init();
  53. let gitlab_client_id = ClientId::new(
  54. env::var("GITLAB_CLIENT_ID").expect("Missing the GITLAB_CLIENT_ID environment variable."),
  55. );
  56. let gitlab_client_secret = ClientSecret::new(
  57. env::var("GITLAB_CLIENT_SECRET")
  58. .expect("Missing the GITLAB_CLIENT_SECRET environment variable."),
  59. );
  60. let issuer_url = IssuerUrl::new("https://gitlab.com".to_string()).expect("Invalid issuer URL");
  61. // Fetch GitLab's OpenID Connect discovery document.
  62. //
  63. let provider_metadata = CoreProviderMetadata::discover(&issuer_url, http_client)
  64. .unwrap_or_else(|err| {
  65. handle_error(&err, "Failed to discover OpenID Provider");
  66. unreachable!();
  67. });
  68. // Set up the config for the GitLab OAuth2 process.
  69. let client = CoreClient::from_provider_metadata(
  70. provider_metadata,
  71. gitlab_client_id,
  72. Some(gitlab_client_secret),
  73. )
  74. // This example will be running its own server at localhost:8080.
  75. // See below for the server implementation.
  76. .set_redirect_uri(
  77. RedirectUrl::new("http://localhost:8080".to_string()).expect("Invalid redirect URL"),
  78. );
  79. // Generate the authorization URL to which we'll redirect the user.
  80. let (authorize_url, csrf_state, nonce) = client
  81. .authorize_url(
  82. AuthenticationFlow::<CoreResponseType>::AuthorizationCode,
  83. CsrfToken::new_random,
  84. Nonce::new_random,
  85. )
  86. // This example is requesting access to the the user's profile including email.
  87. .add_scope(Scope::new("email".to_string()))
  88. .add_scope(Scope::new("profile".to_string()))
  89. .url();
  90. println!("Open this URL in your browser:\n{}\n", authorize_url);
  91. // A very naive implementation of the redirect server.
  92. let listener = TcpListener::bind("127.0.0.1:8080").unwrap();
  93. // Accept one connection
  94. let (mut stream, _) = listener.accept().unwrap();
  95. let code;
  96. let state;
  97. {
  98. let mut reader = BufReader::new(&stream);
  99. let mut request_line = String::new();
  100. reader.read_line(&mut request_line).unwrap();
  101. let redirect_url = request_line.split_whitespace().nth(1).unwrap();
  102. let url = Url::parse(&("http://localhost".to_string() + redirect_url)).unwrap();
  103. let code_pair = url
  104. .query_pairs()
  105. .find(|pair| {
  106. let &(ref key, _) = pair;
  107. key == "code"
  108. })
  109. .unwrap();
  110. let (_, value) = code_pair;
  111. code = AuthorizationCode::new(value.into_owned());
  112. let state_pair = url
  113. .query_pairs()
  114. .find(|pair| {
  115. let &(ref key, _) = pair;
  116. key == "state"
  117. })
  118. .unwrap();
  119. let (_, value) = state_pair;
  120. state = CsrfToken::new(value.into_owned());
  121. }
  122. let message = "Go back to your terminal :)";
  123. let response = format!(
  124. "HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}",
  125. message.len(),
  126. message
  127. );
  128. stream.write_all(response.as_bytes()).unwrap();
  129. println!("GitLab returned the following code:\n{}\n", code.secret());
  130. println!(
  131. "GitLab returned the following state:\n{} (expected `{}`)\n",
  132. state.secret(),
  133. csrf_state.secret()
  134. );
  135. // Exchange the code with a token.
  136. let token_response = client
  137. .exchange_code(code)
  138. .request(http_client)
  139. .unwrap_or_else(|err| {
  140. handle_error(&err, "Failed to contact token endpoint");
  141. unreachable!();
  142. });
  143. println!(
  144. "GitLab returned access token:\n{}\n",
  145. token_response.access_token().secret()
  146. );
  147. println!("GitLab returned scopes: {:?}", token_response.scopes());
  148. let id_token_verifier: CoreIdTokenVerifier = client.id_token_verifier();
  149. let id_token_claims: &CoreIdTokenClaims = token_response
  150. .extra_fields()
  151. .id_token()
  152. .expect("Server did not return an ID token")
  153. .claims(&id_token_verifier, &nonce)
  154. .unwrap_or_else(|err| {
  155. handle_error(&err, "Failed to verify ID token");
  156. unreachable!();
  157. });
  158. println!("GitLab returned ID token: {:?}\n", id_token_claims);
  159. let userinfo_claims: UserInfoClaims<GitLabClaims, CoreGenderClaim> = client
  160. .user_info(token_response.access_token().to_owned(), None)
  161. .unwrap_or_else(|err| {
  162. handle_error(&err, "No user info endpoint");
  163. unreachable!();
  164. })
  165. .request(http_client)
  166. .unwrap_or_else(|err| {
  167. handle_error(&err, "Failed requesting user info");
  168. unreachable!();
  169. });
  170. println!("GitLab returned UserInfo: {:?}", userinfo_claims);
  171. }