Repository: privacyidea/privacyidea

Issue:
Error 500 when validating a HOTP token without a PIN
### Top-level intent

What did you try to achieve?
Validate a HOTP token without a PIN.

### Steps to reproduce

1. Within the WEBUI, enroll a new HOTP token without a PIN.
2. Validate the HOTP token through the /validate/check endpoint.
3. Actual response:
```
{"detail": null, "id": 1, "jsonrpc": "2.0", "result": {"error": {"code": -500, "message": "500 Internal Server Error: The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application."}, "status": false}, "time": 1590439088.7281954, "version": "privacyIDEA 3.3.1", "signature": "rsa_sha256_pss:651f9f22ed62322d44e861d09325bf83aaf1c3b36568bd3e1dcea270da32dc701764462086e4e20fe7c823a3534161a035554fc5e473a20223d1972ec1a33a1f80872b048a951016813a17c836ec1217dff61425c821e1f962659887bd67c3d4ac10727e554e8763b3a242e109faaa8660672fef3be6027c3268ab9da61ab989da1b5219c6f456b3eed2aa8e94ee64e9be2f387118e7f0a1456ab6f5a7203bc047cf9ae9ff67d85a7d3bc586f7b3554cb0bfd3d8c1e592f55892facfb32d137dcd3127f9133725ccc128639ae40b14b949cc982cf29e755dbd551def418124d74ac43905ebe770d00dcdb6597056849b1fcfc7063f5eed680a2a146d08927c4d"}
```

### Expected outcome

What do you think, how the system should have worked?
It should have validated the token with a response like:
```
{"detail": {"message": "matching 1 tokens", "otplen": 6, "serial": "OATH00754647", "threadid": 140556219550336, "type": "hotp"}, "id": 1, "jsonrpc": "2.0", "result": {"status": true, "value": true}, "time": 1590439287.5389853, "version": "privacyIDEA 3.3.1", "versionnumber": "3.3.1", "signature": "rsa_sha256_pss:845ea1b2dcd882e61e3232d1f495edf1e04ee4509d2eea542d317904448ae9c1b64a24124d656948e7ff2938d36fd279f7b410a17dd97cbe29d300b87999afdc41fb893c065c2d003e42a8d6383a8a4729ca634cec37f648e77544520e214ea4c20725b4b40f82eaadadafa9985fac3318f4629d68c5f61f97c8ea123f996a457140fecce9e128e101dc47e74a2500ea647dc97451753564f36ed9c4435f1be413b1643d7747b611fdc292c68ea0b564228bb55c261dd5657fb7869cd8911cd667b800a51cde58f300995ab5d40f1ad0317cdf01e3ccba7128d6887a3633a8b34482dd55af2506cc21eabdda6031409fd1c917295d5228c04d9b1f0753f6707b"}
```

### Actual outcome

What did actually happen?
It answers with an HTTP 500 status code.

### Configuration

* **privacyIDEA version**: 3.3.1
* **Installation method**: github, PyPI, RHEL package dependencies
* **Python version**: 3.6.9
* **Operating system**: RHEL 7.8
* **Webserver**: NGINX Unit 1.17.0
* **Token database**: Oracle 19c (using SQLAlchemy cx_oracle dialect)

### Log file

**Set PI_LOGLEVEL = logging.DEBUG in pi.cfg and take a look at the privacyidea.log!**
**If appropriate, attach the log file or paste relevant portions.**

Attached logfile.
[privacyidea.log](https://github.com/privacyidea/privacyidea/files/4678847/privacyidea.log)




Discussion:
Thank you for filing an issue and sharing your observations or ideas. Please be sure to provide as many information as possible to help us working on this issue.
We can not reproduce this problem and never saw this before. In regards to your PR it looks like as if the ``userObject.pin`` is badly initialized.

Can you tell, which browser you are using?
Hi Cornelius!

We tried using PostgreSQL and it works fine. The problem only seems to occur when using Oracle as the database.

My browser is Chrome 83.0.4103.61 (32 bits build).
Thx! That is really interesting. So I know that Oracle sometimes is strangely picky about data types. It looks like, when no PIN is specified in the WebUI then *none*/NULL  would be passed to privacyIDEA so every other database would store this as an empty string. Only Oracle...
Actually I would prefer to check it when reading the pin from the db. Otherwise if we add an empty pin (i.e. `""` leads to `pin_hash` and `pin_seed` inserted into the db) we could get a number of side effects in the code.
If we check what Oracle returns as an _empty_ entry, we can adjust that part.
Unfortuantely I just cant get the Oracle test db to work... of course there must be a reason for so many high-paid consultants...
@plettich I get your point.
I've submited a new PR #2226 with another approach taking into account your observations.
When no PIN is set, pin_hash in the Token class gets a None value when retrieved from Oracle (I see a NULL value for those records in the database in the PIN_HASH column of the TOKEN table).

For some reason, the default value seems to not being respected when using Oracle:
```python
    pin_hash = db.Column(db.Unicode(512),
                         default=u'')  # hashed
```

Please feel free to close my previous PR #2216 if the new one is more appropriate.
It seems Oracle always interprets an empty string as NULL:
https://stackoverflow.com/questions/203493/why-does-oracle-9i-treat-an-empty-string-as-null
I like Your second approach much better :-)

Resolving patch:
diff --git a/privacyidea/models.py b/privacyidea/models.py
--- a/privacyidea/models.py
+++ b/privacyidea/models.py
@@ -381,11 +381,7 @@ def get_hashed_pin(self, pin):
         log.debug("hPin: {0!s}, pin: {1!r}, seed: {2!s}".format(hPin, pin,
                                                                 self.pin_seed))
         return hPin
-    
-    def check_hashed_pin(self, pin):
-        hp = self.get_hashed_pin(pin)
-        return hp == self.pin_hash
-        
+
     @log_with(log)
     def set_description(self, desc):
         if desc is None:


Test patch:
diff --git a/tests/test_db_model.py b/tests/test_db_model.py
--- a/tests/test_db_model.py
+++ b/tests/test_db_model.py
@@ -79,7 +79,7 @@ def test_00_create_token(self):
         up = t.get_user_pin()
         self.assertTrue(up.getPin() == userpin.encode('utf8'))
 
-        self.assertTrue(t.check_hashed_pin("1234"))
+        self.assertTrue(t.check_pin("1234"))
 
         t.set_user_pin(b'HalloDuDa')
         self.assertTrue(t.get_user_pin().getPin() == b'HalloDuDa')
@@ -88,10 +88,24 @@ def test_00_create_token(self):
         self.assertTrue(t.get_user_pin().getPin().decode('utf8') == u'HelloWörld')
 
         t.set_hashed_pin(b'1234')
-        self.assertTrue(t.check_hashed_pin(b'1234'))
+        self.assertTrue(t.check_pin(b'1234'))
 
         t.set_hashed_pin(u'HelloWörld')
-        self.assertTrue(t.check_hashed_pin(u'HelloWörld'))
+        self.assertTrue(t.check_pin(u'HelloWörld'))
+
+        t.pin_hash = None
+        self.assertTrue(t.check_pin(''))
+        self.assertFalse(t.check_pin(None))
+        self.assertFalse(t.check_pin('1234'))
+
+        t.pin_hash = ''
+        self.assertTrue(t.check_pin(''))
+        self.assertFalse(t.check_pin('1234'))
+
+        t.set_hashed_pin('')
+        self.assertTrue(len(t.pin_hash) > 0)
+        self.assertTrue(t.check_pin(''))
+        self.assertFalse(t.check_pin('1234'))
 
         # Delete the token
         t1.delete()