Integrating swagger and Firebase is really hard because Google token request and response is not standard. So I used the password flow and added a middleware in .NET Core 3.1 Web API to authenticate swagger 5.x. I hope it helps.
The first step is configuring your API to use Firebase for token validation. So we need to add this code to the startup:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.Authority = authority; // this is a url like this "https://securetoken.google.com/{firebase project Id}"
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = authority, //url again
ValidateAudience = true,
ValidAudience = projectId, // {Firebase Project ID}
ValidateLifetime = true
};
options.RequireHttpsMetadata = false;
});
and you need to middleware for Authentication and Authorization in your startup as well.
app.UseAuthentication();
app.UseAuthorization();
Here we go. Now if you use the [Authorize]
attribute on your controller methods and send bearer token in the header it will validate the token by Firebase and works fine.
Now I want to show how you can configure your Swagger to provide the token automatically.
At first, you need to implement a middleware post method to convert Standard Password flow to Google password flow and also convert the result. I did it
[ApiController]
[Route("v1/[controller]")]
public class AuthController : Controller
{
[HttpPost]
public async Task<ActionResult> GetToken([FromForm]LoginInfo loginInfo)
{
string uri = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key={your project api key in firebase}";
using (HttpClient client = new HttpClient())
{
FireBaseLoginInfo fireBaseLoginInfo = new FireBaseLoginInfo
{
Email = loginInfo.Username,
Password = loginInfo.Password
};
var result = await client.PostAsJsonAsync<FireBaseLoginInfo, GoogleToken>(uri, fireBaseLoginInfo);
Token token = new Token
{
token_type = "Bearer",
access_token = result.idToken,
id_token = result.idToken,
expires_in = int.Parse(result.expiresIn),
refresh_token = result.refreshToken
};
return Ok(token);
}
}
}
public class LoginInfo
{
public string Username { get; set; }
public string Password { get; set; }
}
public class FireBaseLoginInfo
{
public string Email { get; set; }
public string Password { get; set; }
public bool ReturnSecureToken { get; set; } = true;
}
public class GoogleToken
{
public string kind { get; set; }
public string localId { get; set; }
public string email { get; set; }
public string displayName { get; set; }
public string idToken { get; set; }
public bool registered { get; set; }
public string refreshToken { get; set; }
public string expiresIn { get; set; }
}
public class Token
{
internal string refresh_token;
public string token_type { get; set; }
public int expires_in { get; set; }
public int ext_expires_in { get; set; }
public string access_token { get; set; }
public string id_token { get; set; }
}
You know we don't have PostAsJsonAsync
in .NET Core, so I add an extension for it.
public static class HttpExtensions
{
public static async Task<R> PostAsJsonAsync<T, R>(
this HttpClient httpClient, string url, T data)
{
var response = await httpClient.PostAsJsonAsync<T>(url, data);
if(response.IsSuccessStatusCode)
{
return await response.Content.ReadAsJsonAsync<R>();
}
else
{
return default(R);
}
}
public static Task<HttpResponseMessage> PostAsJsonAsync<T>(
this HttpClient httpClient, string url, T data)
{
var options = new JsonSerializerSettings
{
ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() },
Formatting = Formatting.Indented
};
var dataAsString = JsonConvert.SerializeObject(data, options);
var content = new StringContent(dataAsString);
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
return httpClient.PostAsync(url, content);
}
public static async Task<T> ReadAsJsonAsync<T>(this HttpContent content)
{
var dataAsString = await content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<T>(dataAsString);
}
}
Now swagger configuration.
services
.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "My Api", Version = "v1" });
c.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows
{
Password = new OpenApiOAuthFlow
{
TokenUrl = new Uri("/v1/auth",UriKind.Relative),
Extensions = new Dictionary<string, IOpenApiExtension>
{
{ "returnSecureToken", new OpenApiBoolean(true) },
},
}
}
});
c.OperationFilter<AuthorizeCheckOperationFilter>();
})
AuthorizeCheckOperationFilter filter is a custom filter
public class AuthorizeCheckOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var requiredScopes = context.MethodInfo.DeclaringType.GetCustomAttributes(true)
.OfType<AuthorizeAttribute>()
.Select(attr => attr.Policy)
.Distinct();
if (requiredScopes.Any())
{
var oAuthScheme = new OpenApiSecurityScheme
{
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" }
};
operation.Security = new List<OpenApiSecurityRequirement>
{
new OpenApiSecurityRequirement
{
[ oAuthScheme ] = requiredScopes.ToList()
}
};
}
}
}
and finally you know that you need to add swagger middleware
app.UseSwagger()
.UseSwaggerUI(c =>
{
c.SwaggerEndpoint($"/swagger/v1/swagger.json", "Chivado Api");
});