Skip to content

Commit

Permalink
added basic password authentication support with email verification
Browse files Browse the repository at this point in the history
  • Loading branch information
albogdano committed Aug 12, 2017
1 parent e5bee7f commit 5fae906
Show file tree
Hide file tree
Showing 9 changed files with 304 additions and 17 deletions.
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ because all the heavy lifting is delegated to Para. This makes the code easy to
### Quick Start

0. You first *need* to create a developer app with [Facebook](https://developers.facebook.com),
[Google](https://console.developers.google.com) or any other identity provider that you wish to use, or enable LDAP.
[Google](https://console.developers.google.com) or any other identity provider that you wish to use.
This isn't necessary only if you enable LDAP or password authentication.

1. Create a new app on [ParaIO.com](https://paraio.com) and save the access keys
2. Click here => [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/Erudika/scoold)
Expand Down Expand Up @@ -76,6 +77,8 @@ para.endpoint = "https://paraio.com"
para.access_key = "app:scoold"
# secret key for your Para app
para.secret_key = "*****************"
# enable or disable email&password authentication
para.password_auth_enabled = false
# needed for geolocation filtering of posts
para.gmaps_api_key = "********************************"
# the identifier of admin user - check Para user object
Expand All @@ -99,7 +102,7 @@ These are set through the Heroku admin panel, under "Settings", "Reveal Config V

This header is enabled by default for enhanced security. It can be disabled with `para.csp_header_enabled = false`.
The default value is modified through `para.csp_header = "new_value"`. The default CSP header is:
```
```ini
default-src 'self';
base-uri 'self';
connect-src 'self' scoold.com www.google-analytics.com;
Expand All @@ -115,7 +118,7 @@ img-src 'self' https: data:; report-uri /reports/cspv
Scoold uses the JavaMail API to send emails. If you want Scoold to send notification emails you should add the
following SMTP settings to your config file:

```
```ini
para.mail.host = "smtp.example.com"
para.mail.port = 587
para.mail.username = "[email protected]"
Expand All @@ -131,7 +134,7 @@ Facebook is the default identity provider for Scoold, and you don't have to spec
For authenticating with Google, you only need your client id (e.g. `123-abcd.apps.googleusercontent.com`).
For all the other providers, GitHub, LinkedIn, Twitter and Microsoft, you need to set both the app id and secret key.
**Note:** if the credentials are blank, the sign in button is hidden for that provider.
```
```ini
# Facebook
para.fb_app_id = "123456789"
# Google
Expand All @@ -150,7 +153,7 @@ para.ms_app_id = ""
para.ms_secret = ""
```
You also need to set your host URL when running Scoold in production:
```
```ini
para.host_url = "https://your.scoold.url"
```
This is required for authentication requests to be redirected back to the origin.
Expand All @@ -160,7 +163,7 @@ This is required for authentication requests to be redirected back to the origin
LDAP authentication is initiated with a request like this `GET /signin?provider=ldap&access_token=username:password`.
There are several configuration options which Para needs in order to connect to your LDAP server. These are the defaults:

```
```ini
para.security.ldap.server_url = "ldap://localhost:8389/"
para.security.ldap.base_dn = "dc=springframework,dc=org"
para.security.ldap.bind_dn = ""
Expand Down
90 changes: 90 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@
"required": false,
"value": ""
},
"para_password_auth_enabled": {
"description": "Enable or disable password authentication",
"required": false,
"value": "false"
},
"para_fb_app_id": {
"description": "Facebook APP ID (for login with Facebook)",
"required": false,
Expand All @@ -71,6 +76,91 @@
"required": false,
"value": ""
},
"para_gh_app_id": {
"description": "GitHub OAuth client id (for login with GitHub)",
"required": false,
"value": ""
},
"para_gh_secret": {
"description": "GitHub OAuth secret",
"required": false,
"value": ""
},
"para_in_app_id": {
"description": "LinkedIn OAuth client id (for login with LinkedIn)",
"required": false,
"value": ""
},
"para_in_secret": {
"description": "LinkedIn OAuth secret",
"required": false,
"value": ""
},
"para_tw_app_id": {
"description": "Twitter OAuth client id (for login with Twitter)",
"required": false,
"value": ""
},
"para_tw_secret": {
"description": "Twitter OAuth secret",
"required": false,
"value": ""
},
"para_ms_app_id": {
"description": "Microsoft OAuth client id (for login with Microsoft)",
"required": false,
"value": ""
},
"para_ms_secret": {
"description": "Microsoft OAuth secret",
"required": false,
"value": ""
},
"para_security_ldap_server_url": {
"description": "LDAP server URL",
"required": false,
"value": ""
},
"para_security_ldap_base_dn": {
"description": "LDAP base DN",
"required": false,
"value": ""
},
"para_security_ldap_bind_dn": {
"description": "LDAP bind DN for connections to server",
"required": false,
"value": ""
},
"para_security_ldap_bind_pass": {
"description": "LDAP bind password for connections to server",
"required": false,
"value": ""
},
"para_security_ldap_user_search_base": {
"description": "LDAP user search base",
"required": false,
"value": ""
},
"para_security_ldap_user_search_filter": {
"description": "LDAP user search filter",
"required": false,
"value": ""
},
"para_security_ldap_user_dn_pattern": {
"description": "LDAP user DN pattern",
"required": false,
"value": ""
},
"para_security_ldap_password_attribute": {
"description": "LDAP password attribute",
"required": false,
"value": ""
},
"para_security_ldap_active_directory_domain": {
"description": "LDAP Active Directory domain (set this only if you are connecting to AD servers)",
"required": false,
"value": ""
},
"para_google_analytics_id": {
"description": "Google Analytics ID for the GA snippet (leave blank to disable GA)",
"required": false,
Expand Down
34 changes: 27 additions & 7 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,30 +13,29 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<link href="https://static.scoold.com/logosq.png" rel="image_src">
<link href="https://static.scoold.com/logosq.png" rel="apple-touch-icon">
<link href="/favicon.ico" rel="shortcut icon">
<link href="https://cdnjs.cloudflare.com/ajax/libs/simplemde/1.11.2/simplemde.min.css" rel="stylesheet">
<link href="https://static.scoold.com/favicon.ico" rel="shortcut icon">
<link href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.98.2/css/materialize.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<meta name="description" content="Scoold is an open source Questions &amp; Answers platform written in Java.">
<meta name="keywords" content="scoold, knowledge sharing, collaboration, wiki, forum, Q&amp;A, questions and answers, stackoverflow clone">
<!-- Google Plus -->
<meta itemprop="name" content="Scoold" itemtype="https://schema.org/Article">
<meta itemprop="description" content="Scoold is an open source Q&A platform written in Java.">
<meta itemprop="image" content="https://scoold.com/scoold-grab-site.png">
<meta itemprop="image" content="https://static.scoold.com/scoold-grab-site.png">
<!-- Twitter -->
<meta name="twitter:card" content="Scoold is an open source Q&A platform written in Java.">
<meta name="twitter:site" content="@getscoold">
<meta name="twitter:title" content="Scoold">
<meta name="twitter:description" content="Scoold is an open source Q&A platform written in Java.">
<meta name="twitter:creator" content="">
<meta name="twitter:image:src" content="https://scoold.com/scoold-grab-site.png">
<meta name="twitter:image:src" content="https://static.scoold.com/scoold-grab-site.png">
<meta name="twitter:player" content="">
<!-- Open Graph General (Facebook & Pinterest) -->
<meta property="og:url" content="https://scoold.com">
<meta property="og:title" content="Scoold">
<meta property="og:description" content="Scoold is an open source Q&A platform written in Java.">
<meta property="og:site_name" content="Scoold Q&A">
<meta property="og:image" content="https://scoold.com/scoold-grab-site.png">
<meta property="og:image" content="https://static.scoold.com/scoold-grab-site.png">
<meta property="fb:app_id" content="99517177417">

<style media="screen" type="text/css">
Expand Down Expand Up @@ -186,7 +185,7 @@ <h5 class="white pam" style="line-height: 1.5em;">
</div>
</div>
<div class="col m8">
<img src="scoold-grab-site.png" width="900" height="" alt="Scoold screenshot"/>
<img src="https://static.scoold.com/scoold-grab-site.png" width="900" height="" alt="Scoold screenshot"/>
</div>
</div>
</div>
Expand All @@ -213,7 +212,7 @@ <h5>Ready for cloud deployment</h5>
<div class="col m4">
<h5>Full-text search</h5>
<hr style="width: 50%">
<p>Each post is indexed and analyzed by Para, which uses Elasticsearch &mdash; a powerful search engine.
<p>Each post is indexed and analyzed by Para and Elasticsearch &mdash; a powerful search engine.
You can search for users, questions and answers from the search bar on top.</p>
</div>
</div>
Expand All @@ -238,6 +237,27 @@ <h5>Classic, light frontend with jQuery</h5>
It takes care of AJAX requests and toggles elements on the screen, nothing more.</p>
</div>
</div>
<div class="row container center-align pvl">
<div class="col m4">
<h5>Hints &amp; similar posts sidebar</h5>
<hr style="width: 50%">
<p>When you ask a new question, Scoold will give you hints if a similar question already exist. Aditionally,
on each question page there's a sidebar with suggestions for the top 10 similar posts.</p>
</div>
<div class="col m4">
<h5>Email notifications</h5>
<hr style="width: 50%">
<p>You can subscribe to each question thread by clicking the checkbox at the bottom. Scoold will send you
emails whenever a new answer is published. Also you can subscribe to replies to your own answers from the
settings page.</p>
</div>
<div class="col m4">
<h5>LDAP authentication support</h5>
<hr style="width: 50%">
<p>Scoold is enterprise-friendly and supports LDAP. It's good for intranet deployments behind corporate
firewalls and Scoold users can be seemlessly authenticated by your company's LDAP server.</p>
</div>
</div>
<div class="row container center-align pvl">
<div class="col m4">
<h5>Modern and responsive layout</h5>
Expand Down
77 changes: 77 additions & 0 deletions src/main/java/com/erudika/scoold/controllers/SigninController.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

package com.erudika.scoold.controllers;

import com.erudika.para.core.Sysprop;
import com.erudika.para.core.User;
import com.erudika.para.utils.Config;
import com.erudika.para.utils.Utils;
Expand All @@ -26,6 +27,7 @@
import static com.erudika.scoold.ScooldServer.signinlink;
import com.erudika.scoold.utils.HttpUtils;
import com.erudika.scoold.utils.ScooldUtils;
import java.util.Collections;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
Expand Down Expand Up @@ -68,6 +70,7 @@ public String get(@RequestParam(name = "returnto", required = false, defaultValu
model.addAttribute("twLoginEnabled", !Config.TWITTER_APP_ID.isEmpty());
model.addAttribute("msLoginEnabled", !Config.MICROSOFT_APP_ID.isEmpty());
model.addAttribute("ldapLoginEnabled", !Config.getConfigParam("security.ldap.server_url", "").isEmpty());
model.addAttribute("passwordLoginEnabled", Config.getConfigBoolean("password_auth_enabled", false));
return "base";
}

Expand All @@ -79,6 +82,7 @@ public String getAuth(@RequestParam("access_token") String accessToken, @Request
if (u != null) {
HttpUtils.setStateParam(Config.AUTH_COOKIE, u.getPassword(), req, res, true);
} else {
verifyEmailIfNecessary(provider, "Anonymous", accessToken.split(":")[0], req);
return "redirect:" + signinlink + "?code=3&error=true";
}
}
Expand All @@ -95,6 +99,51 @@ public String signinSuccess(@RequestParam String jwt, HttpServletRequest req, Ht
return "redirect:" + getBackToUrl(req);
}

@GetMapping(path = "/signin/register")
public String register(@RequestParam(name = "verify", required = false, defaultValue = "false") Boolean verify,
@RequestParam(name = "id", required = false) String id,
@RequestParam(name = "token", required = false) String token,
HttpServletRequest req, Model model) {
if (utils.isAuthenticated(req)) {
return "redirect:" + HOMEPAGE;
}
model.addAttribute("path", "signin.vm");
model.addAttribute("title", utils.getLang(req).get("signup.title"));
model.addAttribute("signinSelected", "navbtn-hover");
model.addAttribute("register", true);
model.addAttribute("verify", verify);
if (id != null && token != null) {
boolean verified = activateWithEmailToken((User) utils.getParaClient().read(id), token);
if (verified) {
model.addAttribute("verified", verified);
} else {
return "redirect:" + signinlink;
}
}
return "base";
}

@PostMapping("/signin/register")
public String signup(@RequestParam String name, @RequestParam String email, @RequestParam String passw,
HttpServletRequest req, Model model) {
if (!utils.isAuthenticated(req)) {
if (utils.getParaClient().read(email) == null) {
utils.getParaClient().signIn("password", email + ":" + name + ":" + passw, false);
verifyEmailIfNecessary("password", name, email, req);
} else {
model.addAttribute("path", "signin.vm");
model.addAttribute("title", utils.getLang(req).get("signup.title"));
model.addAttribute("signinSelected", "navbtn-hover");
model.addAttribute("register", true);
model.addAttribute("name", name);
model.addAttribute("bademail", email);
model.addAttribute("error", Collections.singletonMap("email", utils.getLang(req).get("msgcode.1")));
return "base";
}
}
return "redirect:" + signinlink + "/register?verify=true";
}

@PostMapping("/signout")
public String post(HttpServletRequest req, HttpServletResponse res) {
if (utils.isAuthenticated(req)) {
Expand Down Expand Up @@ -126,4 +175,32 @@ private String getBackToUrl(HttpServletRequest req) {
String backtoFromCookie = Utils.urlDecode(HttpUtils.getStateParam("returnto", req));
return (StringUtils.isBlank(backtoFromCookie) ? HOMEPAGE : backtoFromCookie);
}

private boolean activateWithEmailToken(User u, String token) {
if (u != null && token != null) {
Sysprop s = utils.getParaClient().read(u.getIdentifier());
if (s != null && token.equals(s.getProperty(Config._EMAIL_TOKEN))) {
s.addProperty(Config._EMAIL_TOKEN, "");
utils.getParaClient().update(s);
u.setActive(true);
utils.getParaClient().update(u);
return true;
}
}
return false;
}

private void verifyEmailIfNecessary(String provider, String name, String email, HttpServletRequest req) {
if ("password".equals(provider)) {
Sysprop ident = utils.getParaClient().read(email);
if (ident != null && !ident.hasProperty(Config._EMAIL_TOKEN)) {
User u = new User(ident.getCreatorid());
u.setActive(false);
u.setName(name);
u.setEmail(email);
u.setIdentifier(email);
utils.sendWelcomeEmail(u, true, req);
}
}
}
}
Loading

0 comments on commit 5fae906

Please sign in to comment.