Search code examples
.net-coreoauth-2.0mastodon

Obtain user email from Mastodon API via OAuth


Is it possible to obtain user email through Mastodon Api? I'm working on adding OAuth authentication via Mastodon Api but only seem to get "id" and "display_name" using "/api/v1/accounts/verify_credentials" endpoint. I do not see a property returned for email so currently just using "acct" parameter. I'm using both "read:accounts" and "admin:read:accounts" scopes. This is for an NetCore application.

            builder.Services.AddAuthentication()
                .AddMicrosoftAccount("Microsoft", "Microsoft", microsoftOptions =>
                {
                    microsoftOptions.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
                    microsoftOptions.ClientId = _appSettings.Authentication.Microsoft.ClientId;
                    microsoftOptions.ClientSecret = _appSettings.Authentication.Microsoft.ClientSecret;
                })
                .AddGoogle("Google", "Google", googleOptions =>
                {
                    googleOptions.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
                    googleOptions.ClientId = _appSettings.Authentication.Google.ClientId;
                    googleOptions.ClientSecret = _appSettings.Authentication.Google.ClientSecret;
                })
                .AddGitHub("GitHub", "GitHub", githubOptions =>
                {
                    githubOptions.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
                    githubOptions.ClientId = _appSettings.Authentication.GitHub.ClientId;
                    githubOptions.ClientSecret = _appSettings.Authentication.GitHub.ClientSecret;
                })
                .AddOAuth("Fosstodon", "Fosstodon", fosstodonOptions =>
                 {
                     fosstodonOptions.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;

                     fosstodonOptions.ClientId = _appSettings.Authentication.Fosstodon.ClientId;
                     fosstodonOptions.ClientSecret = _appSettings.Authentication.Fosstodon.ClientSecret;
                     fosstodonOptions.CallbackPath = new PathString("/signin-fosstodon");

                     fosstodonOptions.AuthorizationEndpoint = _appSettings.Authentication.Fosstodon.AuthorizationEndpoint;
                     fosstodonOptions.TokenEndpoint = _appSettings.Authentication.Fosstodon.TokenEndpoint;
                     fosstodonOptions.UserInformationEndpoint = _appSettings.Authentication.Fosstodon.UserInformationEndpoint;

                     fosstodonOptions.SaveTokens = true;
                     fosstodonOptions.Scope.Add("read:accounts");
                     fosstodonOptions.Scope.Add("admin:read:accounts");

                     fosstodonOptions.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
                     fosstodonOptions.ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
                     fosstodonOptions.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");

                     fosstodonOptions.Events = new OAuthEvents
                     {
                         OnCreatingTicket = async context =>
                         {
                             var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
                             request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                             request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);

                             var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
                             response.EnsureSuccessStatusCode();

                             var user = JObject.Parse(await response.Content.ReadAsStringAsync());

                             var identifier = user.Value<string>("id")?.Clean();
                             if (!string.IsNullOrEmpty(identifier))
                             {
                                 context.Identity?.AddClaim(new Claim(
                                     ClaimTypes.NameIdentifier, identifier,
                                     ClaimValueTypes.String, context.Options.ClaimsIssuer));
                             }

                             var userName = user.Value<string>("display_name")?.Clean();
                             if (!string.IsNullOrEmpty(userName))
                             {
                                 context.Identity?.AddClaim(new Claim(
                                     ClaimTypes.Name, userName,
                                     ClaimValueTypes.String, context.Options.ClaimsIssuer));
                             }

                             var userEmail = user.Value<string>("acct")?.Clean();
                             if (!string.IsNullOrEmpty(userEmail))
                             {
                                 context.Identity?.AddClaim(new Claim(
                                     ClaimTypes.Email, userEmail,
                                     ClaimValueTypes.String, context.Options.ClaimsIssuer));
                             }
                         }
                     };
                 });

Really wanting to know if their is an endpoint that will return current user email address. I have looked through Mastodon Api documentation but not seeing and enpoint for this.


Solution

  • Found that it is not posible to obtain user email through Mastodon Api. Instead decided to handel this situation in the registration process of application authentication flow. Basicly check if a user exists with given email. If so then remove the auto provision user and add external provider information to existing user. Later will submit verify email, to email address provided, to avoid someone from stealing user account through registration.

            /// <summary>
            /// Handle postback from new registration
            /// </summary>
            /// <returns>IActionResult</returns>
            [AllowAnonymous]
            [HttpPost("Registration/New")]
            [HttpPost("Registration/New/{id?}")]
            [ValidateAntiForgeryToken]
            public virtual async Task<IActionResult> New([Bind(RegistrationViewModel.BindProperties)] RegistrationViewModel model, [FromForm(Name = "Button")] string button)
            {
                // Check if cancled
                if (button.Clean() != "submit")
                    return RedirectToAction("Index", "Home");
    
                // Check email is valid
                if (!model.Email.Clean().IsValidEmail())
                    ModelState.AddModelError(nameof(model.Email), _sharedLocalizer["ErrorMessage.Invalid"]);
    
                if (ModelState.IsValid)
                {
                    // setup results
                    IdentityResult identityResult = new IdentityResult();
    
                    // Check for existing user
                    ApplicationUser user = await _userManager.FindByEmailAsync(model.Email.Clean());
                    if (user != null)
                    {
                        if (user.Id != model.Id.Clean())
                        {
                            ApplicationUser removeUser = await _userManager.FindByIdAsync(model.Id.Clean());
                            if (removeUser != null)
                            {
                                identityResult = await _userManager.DeleteAsync(removeUser);
                                if (!identityResult.Succeeded) throw new Exception(identityResult.Errors.First().Description);
                            }
                        }
                    }
                    else
                    {
                        user = await _userManager.FindByIdAsync(model.Id.Clean());
                        if (user == null)
                            throw new KeyNotFoundException($"[Key]: {nameof(model.Id)} [Value]: {model.Id}");
                    }
    
                    user.DisplayName = model.DisplayName.Clean();
                    user.UserName = model.Email.Clean();
                    user.NormalizedUserName = model.Email.Clean();
                    user.Email = model.Email.Clean();
                    user.NormalizedEmail = model.Email.Clean();
    
                    identityResult = await _userManager.UpdateAsync(user);
                    if (!identityResult.Succeeded) throw new Exception(identityResult.Errors.First().Description);
    
                    if (!string.IsNullOrEmpty(model.ProviderUserId.Clean()))
                    {
                        var userLogins = await _userManager.GetLoginsAsync(user);
                        UserLoginInfo? userLogin = userLogins
                            .Where(x => x.LoginProvider == model.Provider.Clean())
                            .Where(x => x.ProviderKey == model.ProviderUserId.Clean())
                            .FirstOrDefault();
    
                        if (userLogin == null)
                        {
                            identityResult = await _userManager.AddLoginAsync(user, new UserLoginInfo(model.Provider.Clean(), model.ProviderUserId.Clean(), model.Provider.Clean()));
                            if (!identityResult.Succeeded) throw new Exception(identityResult.Errors.First().Description);
                        }
                    }
                }
    
                return View(model);
            }